Add a GpgSignatureVerifier interface, plus a factory to create instances thereof that is provided via the ServiceLoader mechanism. Implement the new interface for BouncyCastle. A verifier maintains an internal LRU cache of previously found public keys to speed up verifying multiple objects (tag or commits). Mergetags are not handled. Provide a new VerifySignatureCommand in org.eclipse.jgit.api together with a factory method Git.verifySignature(). The command can verify signatures on tags or commits, and can be limited to accept only tags or commits. Provide a new public WrongObjectTypeException thrown when the command is limited to either tags or commits and a name resolves to some other object kind. In jgit.pgm, implement "git tag -v", "git log --show-signature", and "git show --show-signature". The output is similar to command-line gpg invoked via git, but not identical. In particular, lines are not prefixed by "gpg:" but by "bc:". Trust levels for public keys are read from the keys' trust packets, not from GPG's internal trust database. A trust packet may or may not be set. Command-line GPG produces more warning lines depending on the trust level, warning about keys with a trust level below "full". There are no unit tests because JGit still doesn't have any setup to do signing unit tests; this would require at least a faked .gpg directory with pre-created key rings and keys, and a way to make the BouncyCastle classes use that directory instead of the default. See bug 547538 and also bug 544847. Tested manually with a small test repository containing signed and unsigned commits and tags, with signatures made with different keys and made by command-line git using GPG 2.2.25 and by JGit using BouncyCastle 1.65. Bug: 547751 Change-Id: If7e34aeed6ca6636a92bf774d893d98f6d459181 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>tags/v5.11.0.202102240950-m3
@@ -9,11 +9,13 @@ Bundle-Localization: plugin | |||
Bundle-Version: 5.11.0.qualifier | |||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 | |||
Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.bcpg.sig;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.gpg;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.openpgp;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)", | |||
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)", |
@@ -0,0 +1 @@ | |||
org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory |
@@ -8,4 +8,11 @@ gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: | |||
gpgNotASigningKey=Secret key ({0}) is not suitable for signing | |||
gpgKeyInfo=GPG Key (fingerprint {0}) | |||
gpgSigningCancelled=Signing was cancelled | |||
nonSignatureError=Signature does not decode into a signature object | |||
signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1} | |||
signatureKeyLookupError=Error occurred while looking for public key | |||
signatureNoKeyInfo=No way to determine a public key from the signature | |||
signatureNoPublicKey=No public key found to verify the signature | |||
signatureParseError=Signature cannot be parsed | |||
signatureVerificationError=Signature verification failed | |||
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available. |
@@ -1,3 +1,12 @@ | |||
/* | |||
* Copyright (C) 2018, 2021 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 org.eclipse.jgit.nls.NLS; | |||
@@ -28,6 +37,13 @@ public final class BCText extends TranslationBundle { | |||
/***/ public String gpgNotASigningKey; | |||
/***/ public String gpgKeyInfo; | |||
/***/ public String gpgSigningCancelled; | |||
/***/ public String nonSignatureError; | |||
/***/ public String signatureInconsistent; | |||
/***/ public String signatureKeyLookupError; | |||
/***/ public String signatureNoKeyInfo; | |||
/***/ public String signatureNoPublicKey; | |||
/***/ public String signatureParseError; | |||
/***/ public String signatureVerificationError; | |||
/***/ public String unableToSignCommitNoSecretKey; | |||
} |
@@ -1,5 +1,5 @@ | |||
/* | |||
* Copyright (C) 2018, 2020 Salesforce and others | |||
* Copyright (C) 2018, 2021 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 | |||
@@ -14,12 +14,14 @@ import static java.nio.file.Files.newInputStream; | |||
import java.io.BufferedInputStream; | |||
import java.io.File; | |||
import java.io.FileNotFoundException; | |||
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.NoSuchFileException; | |||
import java.nio.file.Path; | |||
import java.nio.file.Paths; | |||
import java.security.NoSuchAlgorithmException; | |||
@@ -247,16 +249,32 @@ public class BouncyCastleGpgKeyLocator { | |||
return false; | |||
} | |||
private String toFingerprint(String keyId) { | |||
private static String toFingerprint(String keyId) { | |||
if (keyId.startsWith("0x")) { //$NON-NLS-1$ | |||
return keyId.substring(2); | |||
} | |||
return keyId; | |||
} | |||
private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob) | |||
static PGPPublicKey findPublicKey(String fingerprint, String keySpec) | |||
throws IOException, PGPException { | |||
PGPPublicKey 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 { | |||
String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); | |||
if (keyId.isEmpty()) { | |||
return null; | |||
} | |||
@@ -270,10 +288,11 @@ public class BouncyCastleGpgKeyLocator { | |||
return null; | |||
} | |||
private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob) | |||
private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob, | |||
String keySpec) | |||
throws IOException { | |||
for (UserID userID : keyBlob.getUserIds()) { | |||
if (containsSigningKey(userID.getUserIDAsString(), signingKey)) { | |||
if (containsSigningKey(userID.getUserIDAsString(), keySpec)) { | |||
return getSigningPublicKey(keyBlob); | |||
} | |||
} | |||
@@ -285,6 +304,10 @@ public class BouncyCastleGpgKeyLocator { | |||
* | |||
* @param keyboxFile | |||
* the KeyBox file | |||
* @param keyId | |||
* to look for, may be null | |||
* @param keySpec | |||
* to look for | |||
* @return publicKey the public key (maybe <code>null</code>) | |||
* @throws IOException | |||
* in case of problems reading the file | |||
@@ -293,19 +316,22 @@ public class BouncyCastleGpgKeyLocator { | |||
* @throws NoOpenPgpKeyException | |||
* if the file does not contain any OpenPGP key | |||
*/ | |||
private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile) | |||
private static PGPPublicKey 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); | |||
PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id); | |||
if (key != null) { | |||
return key; | |||
} | |||
key = findPublicKeyByUserId(keyBlob); | |||
key = findPublicKeyByUserId(keyBlob, keySpec); | |||
if (key != null) { | |||
return key; | |||
} | |||
@@ -349,7 +375,8 @@ public class BouncyCastleGpgKeyLocator { | |||
// pubring.gpg also try secring.gpg to find the secret key. | |||
if (exists(USER_KEYBOX_PATH)) { | |||
try { | |||
publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH); | |||
publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null, | |||
signingKey); | |||
if (publicKey != null) { | |||
key = findSecretKeyForKeyBoxPublicKey(publicKey, | |||
USER_KEYBOX_PATH); | |||
@@ -372,7 +399,8 @@ public class BouncyCastleGpgKeyLocator { | |||
} | |||
} | |||
if (exists(USER_PGP_PUBRING_FILE)) { | |||
publicKey = findPublicKeyInPubring(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 | |||
@@ -562,6 +590,11 @@ public class BouncyCastleGpgKeyLocator { | |||
* 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 | |||
@@ -569,14 +602,16 @@ public class BouncyCastleGpgKeyLocator { | |||
* @throws PGPException | |||
* on BouncyCastle errors | |||
*/ | |||
private PGPPublicKey findPublicKeyInPubring(Path pubringFile) | |||
private static PGPPublicKey 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 keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); | |||
String id = keyId != null ? keyId | |||
: toFingerprint(keySpec).toLowerCase(Locale.ROOT); | |||
Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings(); | |||
while (keyrings.hasNext()) { | |||
PGPPublicKeyRing keyRing = keyrings.next(); | |||
@@ -586,30 +621,33 @@ public class BouncyCastleGpgKeyLocator { | |||
// try key id | |||
String fingerprint = Hex.toHexString(key.getFingerprint()) | |||
.toLowerCase(Locale.ROOT); | |||
if (fingerprint.endsWith(keyId)) { | |||
if (fingerprint.endsWith(id)) { | |||
return key; | |||
} | |||
// try user id | |||
Iterator<String> userIDs = key.getUserIDs(); | |||
while (userIDs.hasNext()) { | |||
String userId = userIDs.next(); | |||
if (containsSigningKey(userId, signingKey)) { | |||
if (containsSigningKey(userId, keySpec)) { | |||
return key; | |||
} | |||
} | |||
} | |||
} | |||
} catch (FileNotFoundException | NoSuchFileException e) { | |||
// Ignore and return null | |||
} | |||
return null; | |||
} | |||
private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint) | |||
private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint) | |||
throws IOException { | |||
return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing() | |||
.getPublicKey(fingerprint); | |||
} | |||
private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException { | |||
private static PGPPublicKey getSigningPublicKey(KeyBlob blob) | |||
throws IOException { | |||
PGPPublicKey masterKey = null; | |||
Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob) | |||
.getPGPPublicKeyRing().getPublicKeys(); | |||
@@ -629,7 +667,7 @@ public class BouncyCastleGpgKeyLocator { | |||
return masterKey; | |||
} | |||
private boolean isSigningKey(PGPPublicKey key) { | |||
private static boolean isSigningKey(PGPPublicKey key) { | |||
Iterator signatures = key.getSignatures(); | |||
while (signatures.hasNext()) { | |||
PGPSignature sig = (PGPSignature) signatures.next(); | |||
@@ -641,7 +679,7 @@ public class BouncyCastleGpgKeyLocator { | |||
return false; | |||
} | |||
private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException, | |||
private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException, | |||
NoSuchAlgorithmException, NoSuchProviderException, | |||
NoOpenPgpKeyException { | |||
if (keyboxFile.toFile().length() == 0) { |
@@ -0,0 +1,388 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.security.Security; | |||
import java.text.MessageFormat; | |||
import java.time.Instant; | |||
import java.util.Arrays; | |||
import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.Locale; | |||
import org.bouncycastle.bcpg.sig.IssuerFingerprint; | |||
import org.bouncycastle.jce.provider.BouncyCastleProvider; | |||
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.annotations.NonNull; | |||
import org.eclipse.jgit.annotations.Nullable; | |||
import org.eclipse.jgit.api.errors.JGitInternalException; | |||
import org.eclipse.jgit.lib.GpgConfig; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevObject; | |||
import org.eclipse.jgit.revwalk.RevTag; | |||
import org.eclipse.jgit.util.LRUMap; | |||
import org.eclipse.jgit.util.RawParseUtils; | |||
import org.eclipse.jgit.util.StringUtils; | |||
/** | |||
* A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle. | |||
*/ | |||
public class BouncyCastleGpgSignatureVerifier implements GpgSignatureVerifier { | |||
private static void registerBouncyCastleProviderIfNecessary() { | |||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { | |||
Security.addProvider(new BouncyCastleProvider()); | |||
} | |||
} | |||
/** | |||
* Creates a new instance and registers the BouncyCastle security provider | |||
* if needed. | |||
*/ | |||
public BouncyCastleGpgSignatureVerifier() { | |||
registerBouncyCastleProviderIfNecessary(); | |||
} | |||
// 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<String, Object> byFingerprint = new LRUMap<>(16, 200); | |||
private LRUMap<String, Object> bySigner = new LRUMap<>(16, 200); | |||
@Override | |||
public String getName() { | |||
return "bc"; //$NON-NLS-1$ | |||
} | |||
@Override | |||
@Nullable | |||
public SignatureVerification verifySignature(@NonNull RevObject object, | |||
@NonNull GpgConfig config) throws IOException { | |||
if (object instanceof RevCommit) { | |||
RevCommit commit = (RevCommit) object; | |||
byte[] signatureData = commit.getRawGpgSignature(); | |||
if (signatureData == null) { | |||
return null; | |||
} | |||
byte[] raw = commit.getRawBuffer(); | |||
// Now remove the GPG signature | |||
byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' }; | |||
int start = RawParseUtils.headerStart(header, raw, 0); | |||
if (start < 0) { | |||
return null; | |||
} | |||
int end = RawParseUtils.headerEnd(raw, start); | |||
// start is at the beginning of the header's content | |||
start -= header.length + 1; | |||
// end is on the terminating LF; we need to skip that, too | |||
if (end < raw.length) { | |||
end++; | |||
} | |||
byte[] data = new byte[raw.length - (end - start)]; | |||
System.arraycopy(raw, 0, data, 0, start); | |||
System.arraycopy(raw, end, data, start, raw.length - end); | |||
return verify(data, signatureData); | |||
} else if (object instanceof RevTag) { | |||
RevTag tag = (RevTag) object; | |||
byte[] signatureData = tag.getRawGpgSignature(); | |||
if (signatureData == null) { | |||
return null; | |||
} | |||
byte[] raw = tag.getRawBuffer(); | |||
// The signature is just tacked onto the end of the message, which | |||
// is last in the buffer. | |||
byte[] data = Arrays.copyOfRange(raw, 0, | |||
raw.length - signatureData.length); | |||
return verify(data, signatureData); | |||
} | |||
return null; | |||
} | |||
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(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 (PGPException e) { | |||
throw new JGitInternalException(BCText.get().signatureParseError, | |||
e); | |||
} | |||
Date signatureCreatedAt = signature.getCreationTime(); | |||
if (fingerprint == null && signer == null && keyId == null) { | |||
return new VerificationResult(signatureCreatedAt, null, null, null, | |||
false, false, TrustLevel.UNKNOWN, | |||
BCText.get().signatureNoKeyInfo); | |||
} | |||
if (fingerprint != null && keyId != null | |||
&& !fingerprint.endsWith(keyId)) { | |||
return new VerificationResult(signatureCreatedAt, signer, fingerprint, | |||
null, 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; | |||
PGPPublicKey publicKey = null; | |||
try { | |||
cached = byFingerprint.get(fingerprint); | |||
if (cached != null) { | |||
if (cached instanceof PGPPublicKey) { | |||
publicKey = (PGPPublicKey) cached; | |||
} | |||
} else if (!StringUtils.isEmptyOrNull(signer)) { | |||
cached = bySigner.get(signer); | |||
if (cached != null) { | |||
if (cached instanceof PGPPublicKey) { | |||
publicKey = (PGPPublicKey) 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 VerificationResult(signatureCreatedAt, signer, | |||
fingerprint, null, false, false, TrustLevel.UNKNOWN, | |||
BCText.get().signatureNoPublicKey); | |||
} | |||
if (cached == null) { | |||
byFingerprint.put(fingerprint, publicKey); | |||
byFingerprint.put(keyId, publicKey); | |||
if (signer != null) { | |||
bySigner.put(signer, publicKey); | |||
} | |||
} | |||
String user = null; | |||
Iterator<String> userIds = publicKey.getUserIDs(); | |||
if (!StringUtils.isEmptyOrNull(signer)) { | |||
while (userIds.hasNext()) { | |||
String userId = userIds.next(); | |||
if (BouncyCastleGpgKeyLocator.containsSigningKey(userId, | |||
keySpec)) { | |||
user = userId; | |||
break; | |||
} | |||
} | |||
} | |||
if (user == null) { | |||
userIds = publicKey.getUserIDs(); | |||
if (userIds.hasNext()) { | |||
user = userIds.next(); | |||
} | |||
} | |||
boolean expired = false; | |||
long validFor = publicKey.getValidSeconds(); | |||
if (validFor > 0 && signatureCreatedAt != null) { | |||
Instant expiredAt = publicKey.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 = publicKey.getTrustData(); | |||
TrustLevel trust = parseGpgTrustPacket(trustData); | |||
boolean verified = false; | |||
try { | |||
signature.init( | |||
new JcaPGPContentVerifierBuilderProvider() | |||
.setProvider(BouncyCastleProvider.PROVIDER_NAME), | |||
publicKey); | |||
signature.update(data); | |||
verified = signature.verify(); | |||
} catch (PGPException e) { | |||
throw new JGitInternalException( | |||
BCText.get().signatureVerificationError, e); | |||
} | |||
return new VerificationResult(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(); | |||
} | |||
private static class VerificationResult implements SignatureVerification { | |||
private final Date creationDate; | |||
private final String signer; | |||
private final String keyUser; | |||
private final String fingerprint; | |||
private final boolean verified; | |||
private final boolean expired; | |||
private final @NonNull TrustLevel trustLevel; | |||
private final String message; | |||
public VerificationResult(Date creationDate, String signer, | |||
String fingerprint, String user, boolean verified, | |||
boolean expired, @NonNull TrustLevel trust, String message) { | |||
this.creationDate = creationDate; | |||
this.signer = signer; | |||
this.fingerprint = fingerprint; | |||
this.keyUser = user; | |||
this.verified = verified; | |||
this.expired = expired; | |||
this.trustLevel = trust; | |||
this.message = message; | |||
} | |||
@Override | |||
public Date getCreationDate() { | |||
return creationDate; | |||
} | |||
@Override | |||
public String getSigner() { | |||
return signer; | |||
} | |||
@Override | |||
public String getKeyUser() { | |||
return keyUser; | |||
} | |||
@Override | |||
public String getKeyFingerprint() { | |||
return fingerprint; | |||
} | |||
@Override | |||
public boolean isExpired() { | |||
return expired; | |||
} | |||
@Override | |||
public TrustLevel getTrustLevel() { | |||
return trustLevel; | |||
} | |||
@Override | |||
public String getMessage() { | |||
return message; | |||
} | |||
@Override | |||
public boolean getVerified() { | |||
return verified; | |||
} | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.GpgSignatureVerifier; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; | |||
/** | |||
* A {@link GpgSignatureVerifierFactory} that creates | |||
* {@link GpgSignatureVerifier} instances that verify GPG signatures using | |||
* BouncyCastle and that do cache public keys. | |||
*/ | |||
public final class BouncyCastleGpgSignatureVerifierFactory | |||
extends GpgSignatureVerifierFactory { | |||
@Override | |||
public GpgSignatureVerifier getVerifier() { | |||
return new BouncyCastleGpgSignatureVerifier(); | |||
} | |||
} |
@@ -1,5 +1,5 @@ | |||
/* | |||
* Copyright (C) 2018, 2020, Salesforce and others | |||
* Copyright (C) 2018, 2021, 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 | |||
@@ -39,9 +39,9 @@ import org.eclipse.jgit.errors.UnsupportedCredentialItem; | |||
import org.eclipse.jgit.internal.JGitText; | |||
import org.eclipse.jgit.lib.CommitBuilder; | |||
import org.eclipse.jgit.lib.GpgConfig; | |||
import org.eclipse.jgit.lib.GpgObjectSigner; | |||
import org.eclipse.jgit.lib.GpgSignature; | |||
import org.eclipse.jgit.lib.GpgSigner; | |||
import org.eclipse.jgit.lib.GpgObjectSigner; | |||
import org.eclipse.jgit.lib.ObjectBuilder; | |||
import org.eclipse.jgit.lib.PersonIdent; | |||
import org.eclipse.jgit.lib.GpgConfig.GpgFormat; | |||
@@ -210,7 +210,7 @@ public class BouncyCastleGpgSigner extends GpgSigner | |||
} | |||
} | |||
private String extractSignerId(String pgpUserId) { | |||
static String extractSignerId(String pgpUserId) { | |||
int from = pgpUserId.indexOf('<'); | |||
if (from >= 0) { | |||
int to = pgpUserId.indexOf('>', from + 1); |
@@ -77,14 +77,15 @@ invalidHttpProxyOnlyHttpSupported=Invalid http_proxy: {0}: Only http supported. | |||
invalidRecurseSubmodulesMode=Invalid recurse submodules mode: {0} | |||
invalidUntrackedFilesMode=Invalid untracked files mode ''{0}'' | |||
jgitVersion=jgit version {0} | |||
lineFormat={0} | |||
listeningOn=Listening on {0} | |||
lfsNoAccessKey=No accessKey in {0} | |||
lfsNoSecretKey=No secretKey in {0} | |||
lfsProtocolUrl=LFS protocol URL: {0} | |||
lfsStoreDirectory=LFS objects stored in: {0} | |||
lfsStoreUrl=LFS store URL: {0} | |||
lfsUnknownStoreType="Unknown LFS store type: {0}" | |||
lineFormat={0} | |||
listeningOn=Listening on {0} | |||
logNoSignatureVerifier="No signature verifier available" | |||
mergeConflict=CONFLICT(content): Merge conflict in {0} | |||
mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge: | |||
mergeFailed=Automatic merge failed; fix conflicts and then commit the result | |||
@@ -411,6 +412,7 @@ usage_show=Display one commit | |||
usage_showRefNamesMatchingCommits=Show ref names matching commits | |||
usage_showPatch=display patch | |||
usage_showNotes=Add this ref to the list of note branches from which notes are displayed | |||
usage_showSignature=Verify signatures of signed commits in the log | |||
usage_showTimeInMilliseconds=Show mtime in milliseconds | |||
usage_squash=Squash commits as if a real merge happened, but do not make a commit or move the HEAD. | |||
usage_srcPrefix=show the source prefix instead of "a/" | |||
@@ -424,6 +426,7 @@ usage_tagLocalUser=create a signed annotated tag using the specified GPG key ID | |||
usage_tagMessage=create an annotated tag with the given message, unsigned unless -s or -u are given, or config tag.gpgSign is true, or tar.forceSignAnnotated is true and -a is not given | |||
usage_tagSign=create a signed annotated tag | |||
usage_tagNoSign=suppress signing the tag | |||
usage_tagVerify=Verify the GPG signature | |||
usage_untrackedFilesMode=show untracked files | |||
usage_updateRef=reference to update | |||
usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository |
@@ -1,7 +1,7 @@ | |||
/* | |||
* Copyright (C) 2010, Google Inc. | |||
* Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com> | |||
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others | |||
* Copyright (C) 2006, 2008, Robin Rosenberg <robin.rosenberg@dewire.com> | |||
* Copyright (C) 2008, 2021, Shawn O. Pearce <spearce@spearce.org> 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 | |||
@@ -31,12 +31,17 @@ import org.eclipse.jgit.diff.RenameDetector; | |||
import org.eclipse.jgit.errors.LargeObjectException; | |||
import org.eclipse.jgit.lib.AnyObjectId; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.GpgConfig; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.PersonIdent; | |||
import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.notes.NoteMap; | |||
import org.eclipse.jgit.pgm.internal.CLIText; | |||
import org.eclipse.jgit.pgm.internal.VerificationUtils; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevTree; | |||
import org.eclipse.jgit.util.GitDateFormatter; | |||
@@ -68,6 +73,9 @@ class Log extends RevWalkTextBuiltin { | |||
additionalNoteRefs.add(notesRef); | |||
} | |||
@Option(name = "--show-signature", usage = "usage_showSignature") | |||
private boolean showSignature; | |||
@Option(name = "--date", usage = "usage_date") | |||
void dateFormat(String date) { | |||
if (date.toLowerCase(Locale.ROOT).equals(date)) | |||
@@ -147,6 +155,10 @@ class Log extends RevWalkTextBuiltin { | |||
// END -- Options shared with Diff | |||
private GpgSignatureVerifier verifier; | |||
private GpgConfig config; | |||
Log() { | |||
dateFormatter = new GitDateFormatter(Format.DEFAULT); | |||
} | |||
@@ -161,6 +173,7 @@ class Log extends RevWalkTextBuiltin { | |||
/** {@inheritDoc} */ | |||
@Override | |||
protected void run() { | |||
config = new GpgConfig(db.getConfig()); | |||
diffFmt.setRepository(db); | |||
try { | |||
diffFmt.setPathFilter(pathFilter); | |||
@@ -197,6 +210,9 @@ class Log extends RevWalkTextBuiltin { | |||
throw die(e.getMessage(), e); | |||
} finally { | |||
diffFmt.close(); | |||
if (verifier != null) { | |||
verifier.clear(); | |||
} | |||
} | |||
} | |||
@@ -229,6 +245,9 @@ class Log extends RevWalkTextBuiltin { | |||
} | |||
outw.println(); | |||
if (showSignature) { | |||
showSignature(c); | |||
} | |||
final PersonIdent author = c.getAuthorIdent(); | |||
outw.println(MessageFormat.format(CLIText.get().authorInfo, author.getName(), author.getEmailAddress())); | |||
outw.println(MessageFormat.format(CLIText.get().dateInfo, | |||
@@ -252,6 +271,27 @@ class Log extends RevWalkTextBuiltin { | |||
outw.flush(); | |||
} | |||
private void showSignature(RevCommit c) throws IOException { | |||
if (c.getRawGpgSignature() == null) { | |||
return; | |||
} | |||
if (verifier == null) { | |||
GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory | |||
.getDefault(); | |||
if (factory == null) { | |||
throw die(CLIText.get().logNoSignatureVerifier, null); | |||
} | |||
verifier = factory.getVerifier(); | |||
} | |||
SignatureVerification verification = verifier.verifySignature(c, | |||
config); | |||
if (verification == null) { | |||
return; | |||
} | |||
VerificationUtils.writeVerification(outw, verification, | |||
verifier.getName(), c.getCommitterIdent()); | |||
} | |||
/** | |||
* @param c | |||
* @return <code>true</code> if at least one note was printed, |
@@ -29,10 +29,15 @@ import org.eclipse.jgit.errors.MissingObjectException; | |||
import org.eclipse.jgit.errors.RevisionSyntaxException; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.FileMode; | |||
import org.eclipse.jgit.lib.GpgConfig; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.PersonIdent; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; | |||
import org.eclipse.jgit.pgm.internal.CLIText; | |||
import org.eclipse.jgit.pgm.internal.VerificationUtils; | |||
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevObject; | |||
@@ -59,6 +64,9 @@ class Show extends TextBuiltin { | |||
@Option(name = "--", metaVar = "metaVar_path", handler = PathTreeFilterHandler.class) | |||
protected TreeFilter pathFilter = TreeFilter.ALL; | |||
@Option(name = "--show-signature", usage = "usage_showSignature") | |||
private boolean showSignature; | |||
// BEGIN -- Options shared with Diff | |||
@Option(name = "-p", usage = "usage_showPatch") | |||
boolean showPatch; | |||
@@ -220,13 +228,16 @@ class Show extends TextBuiltin { | |||
} | |||
outw.println(); | |||
String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$ | |||
for (String s : lines) { | |||
outw.println(s); | |||
String fullMessage = tag.getFullMessage(); | |||
if (!fullMessage.isEmpty()) { | |||
String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$ | |||
for (String s : lines) { | |||
outw.println(s); | |||
} | |||
} | |||
byte[] rawSignature = tag.getRawGpgSignature(); | |||
if (rawSignature != null) { | |||
lines = RawParseUtils.decode(rawSignature).split("\n"); //$NON-NLS-1$ | |||
String[] lines = RawParseUtils.decode(rawSignature).split("\n"); //$NON-NLS-1$ | |||
for (String s : lines) { | |||
outw.println(s); | |||
} | |||
@@ -258,6 +269,10 @@ class Show extends TextBuiltin { | |||
c.getId().copyTo(outbuffer, outw); | |||
outw.println(); | |||
if (showSignature) { | |||
showSignature(c); | |||
} | |||
final PersonIdent author = c.getAuthorIdent(); | |||
outw.println(MessageFormat.format(CLIText.get().authorInfo, | |||
author.getName(), author.getEmailAddress())); | |||
@@ -296,4 +311,28 @@ class Show extends TextBuiltin { | |||
} | |||
outw.println(); | |||
} | |||
private void showSignature(RevCommit c) throws IOException { | |||
if (c.getRawGpgSignature() == null) { | |||
return; | |||
} | |||
GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory | |||
.getDefault(); | |||
if (factory == null) { | |||
throw die(CLIText.get().logNoSignatureVerifier, null); | |||
} | |||
GpgSignatureVerifier verifier = factory.getVerifier(); | |||
GpgConfig config = new GpgConfig(db.getConfig()); | |||
try { | |||
SignatureVerification verification = verifier.verifySignature(c, | |||
config); | |||
if (verification == null) { | |||
return; | |||
} | |||
VerificationUtils.writeVerification(outw, verification, | |||
verifier.getName(), c.getCommitterIdent()); | |||
} finally { | |||
verifier.clear(); | |||
} | |||
} | |||
} |
@@ -4,7 +4,7 @@ | |||
* Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org> | |||
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg.lists@dewire.com> | |||
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> | |||
* Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others | |||
* Copyright (C) 2008, 2021 Shawn O. Pearce <spearce@spearce.org> 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 | |||
@@ -22,43 +22,60 @@ import java.util.List; | |||
import org.eclipse.jgit.api.Git; | |||
import org.eclipse.jgit.api.ListTagCommand; | |||
import org.eclipse.jgit.api.TagCommand; | |||
import org.eclipse.jgit.api.VerificationResult; | |||
import org.eclipse.jgit.api.VerifySignatureCommand; | |||
import org.eclipse.jgit.api.errors.GitAPIException; | |||
import org.eclipse.jgit.api.errors.RefAlreadyExistsException; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.pgm.internal.CLIText; | |||
import org.eclipse.jgit.pgm.internal.VerificationUtils; | |||
import org.eclipse.jgit.revwalk.RevTag; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
import org.kohsuke.args4j.Argument; | |||
import org.kohsuke.args4j.Option; | |||
@Command(common = true, usage = "usage_CreateATag") | |||
class Tag extends TextBuiltin { | |||
@Option(name = "-f", usage = "usage_forceReplacingAnExistingTag") | |||
@Option(name = "--force", aliases = { "-f" }, forbids = { "--delete", | |||
"--verify" }, usage = "usage_forceReplacingAnExistingTag") | |||
private boolean force; | |||
@Option(name = "-d", usage = "usage_tagDelete") | |||
@Option(name = "--delete", aliases = { "-d" }, forbids = { | |||
"--verify" }, usage = "usage_tagDelete") | |||
private boolean delete; | |||
@Option(name = "--annotate", aliases = { | |||
"-a" }, usage = "usage_tagAnnotated") | |||
"-a" }, forbids = { "--delete", | |||
"--verify" }, usage = "usage_tagAnnotated") | |||
private boolean annotated; | |||
@Option(name = "-m", metaVar = "metaVar_message", usage = "usage_tagMessage") | |||
@Option(name = "-m", forbids = { "--delete", | |||
"--verify" }, metaVar = "metaVar_message", usage = "usage_tagMessage") | |||
private String message; | |||
@Option(name = "--sign", aliases = { "-s" }, forbids = { | |||
"--no-sign" }, usage = "usage_tagSign") | |||
"--no-sign", "--delete", "--verify" }, usage = "usage_tagSign") | |||
private boolean sign; | |||
@Option(name = "--no-sign", usage = "usage_tagNoSign", forbids = { | |||
"--sign" }) | |||
"--sign", "--delete", "--verify" }) | |||
private boolean noSign; | |||
@Option(name = "--local-user", aliases = { | |||
"-u" }, metaVar = "metaVar_tagLocalUser", usage = "usage_tagLocalUser") | |||
"-u" }, forbids = { "--delete", | |||
"--verify" }, metaVar = "metaVar_tagLocalUser", usage = "usage_tagLocalUser") | |||
private String gpgKeyId; | |||
@Option(name = "--verify", aliases = { "-v" }, forbids = { "--delete", | |||
"--force", "--annotate", "-m", "--sign", "--no-sign", | |||
"--local-user" }, usage = "usage_tagVerify") | |||
private boolean verify; | |||
@Argument(index = 0, metaVar = "metaVar_name") | |||
private String tagName; | |||
@@ -70,7 +87,25 @@ class Tag extends TextBuiltin { | |||
protected void run() { | |||
try (Git git = new Git(db)) { | |||
if (tagName != null) { | |||
if (delete) { | |||
if (verify) { | |||
VerifySignatureCommand verifySig = git.verifySignature() | |||
.setMode(VerifySignatureCommand.VerifyMode.TAGS) | |||
.addName(tagName); | |||
VerificationResult verification = verifySig.call() | |||
.get(tagName); | |||
if (verification == null) { | |||
showUnsigned(git, tagName); | |||
} else { | |||
Throwable error = verification.getException(); | |||
if (error != null) { | |||
throw die(error.getMessage(), error); | |||
} | |||
writeVerification(verifySig.getVerifier().getName(), | |||
(RevTag) verification.getObject(), | |||
verification.getVerification()); | |||
} | |||
} else if (delete) { | |||
List<String> deletedTags = git.tagDelete().setTags(tagName) | |||
.call(); | |||
if (deletedTags.isEmpty()) { | |||
@@ -116,4 +151,36 @@ class Tag extends TextBuiltin { | |||
throw die(e.getMessage(), e); | |||
} | |||
} | |||
private void showUnsigned(Git git, String wantedTag) throws IOException { | |||
ObjectId id = git.getRepository().resolve(wantedTag); | |||
if (id != null && !ObjectId.zeroId().equals(id)) { | |||
try (RevWalk walk = new RevWalk(git.getRepository())) { | |||
showTag(walk.parseTag(id)); | |||
} | |||
} else { | |||
throw die( | |||
MessageFormat.format(CLIText.get().tagNotFound, wantedTag)); | |||
} | |||
} | |||
private void showTag(RevTag tag) throws IOException { | |||
outw.println("object " + tag.getObject().name()); //$NON-NLS-1$ | |||
outw.println("type " + Constants.typeString(tag.getObject().getType())); //$NON-NLS-1$ | |||
outw.println("tag " + tag.getTagName()); //$NON-NLS-1$ | |||
outw.println("tagger " + tag.getTaggerIdent().toExternalString()); //$NON-NLS-1$ | |||
outw.println(); | |||
outw.print(tag.getFullMessage()); | |||
} | |||
private void writeVerification(String name, RevTag tag, | |||
SignatureVerification verification) throws IOException { | |||
showTag(tag); | |||
if (verification == null) { | |||
outw.println(); | |||
return; | |||
} | |||
VerificationUtils.writeVerification(outw, verification, name, | |||
tag.getTaggerIdent()); | |||
} | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com> | |||
* Copyright (C) 2013, Obeo and others | |||
* Copyright (C) 2013, 2021 Obeo 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 | |||
@@ -163,6 +163,7 @@ public class CLIText extends TranslationBundle { | |||
/***/ public String lfsUnknownStoreType; | |||
/***/ public String lineFormat; | |||
/***/ public String listeningOn; | |||
/***/ public String logNoSignatureVerifier; | |||
/***/ public String mergeCheckoutConflict; | |||
/***/ public String mergeConflict; | |||
/***/ public String mergeFailed; |
@@ -0,0 +1,56 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.pgm.internal; | |||
import java.io.IOException; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; | |||
import org.eclipse.jgit.lib.PersonIdent; | |||
import org.eclipse.jgit.util.GitDateFormatter; | |||
import org.eclipse.jgit.util.SignatureUtils; | |||
import org.eclipse.jgit.util.io.ThrowingPrintWriter; | |||
/** | |||
* Utilities for signature verification. | |||
*/ | |||
public final class VerificationUtils { | |||
private VerificationUtils() { | |||
// No instantiation | |||
} | |||
/** | |||
* Writes information about a signature verification to the given writer. | |||
* | |||
* @param out | |||
* to write to | |||
* @param verification | |||
* to show | |||
* @param name | |||
* of the verifier used | |||
* @param creator | |||
* of the object verified; used for time zone information | |||
* @throws IOException | |||
* if writing fails | |||
*/ | |||
public static void writeVerification(ThrowingPrintWriter out, | |||
SignatureVerification verification, String name, | |||
PersonIdent creator) throws IOException { | |||
String[] text = SignatureUtils | |||
.toString(verification, creator, | |||
new GitDateFormatter(GitDateFormatter.Format.LOCALE)) | |||
.split("\n"); //$NON-NLS-1$ | |||
for (String line : text) { | |||
out.print(name); | |||
out.print(": "); //$NON-NLS-1$ | |||
out.println(line); | |||
} | |||
} | |||
} |
@@ -622,6 +622,8 @@ shortCompressedStreamAt=Short compressed stream at {0} | |||
shortReadOfBlock=Short read of block. | |||
shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section. | |||
shortSkipOfBlock=Short skip of block. | |||
signatureVerificationError=Signature verification failed | |||
signatureVerificationUnavailable=No signature verifier registered | |||
signedTagMessageNoLf=A non-empty message of a signed tag must end in LF. | |||
signingServiceUnavailable=Signing service is not available | |||
similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100. | |||
@@ -763,6 +765,13 @@ uriNotFoundWithMessage={0} not found: {1} | |||
URINotSupported=URI not supported: {0} | |||
userConfigInvalid=Git config in the user's home directory {0} is invalid {1} | |||
validatingGitModules=Validating .gitmodules files | |||
verifySignatureBad=BAD signature from "{0}" | |||
verifySignatureExpired=Expired signature from "{0}" | |||
verifySignatureGood=Good signature from "{0}" | |||
verifySignatureIssuer=issuer "{0}" | |||
verifySignatureKey=using key {0} | |||
verifySignatureMade=Signature made {0} | |||
verifySignatureTrust=[{0}] | |||
walkFailure=Walk failure. | |||
wantNoSpaceWithCapabilities=No space between oid and first capability in first want line | |||
wantNotValid=want {0} not valid |
@@ -1,6 +1,6 @@ | |||
/* | |||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | |||
* Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others | |||
* Copyright (C) 2010, 2021 Chris Aniszczyk <caniszczyk@gmail.com> 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 | |||
@@ -772,6 +772,16 @@ public class Git implements AutoCloseable { | |||
return new RemoteSetUrlCommand(repo); | |||
} | |||
/** | |||
* Return a command to verify signatures of tags or commits. | |||
* | |||
* @return a {@link VerifySignatureCommand} | |||
* @since 5.11 | |||
*/ | |||
public VerifySignatureCommand verifySignature() { | |||
return new VerifySignatureCommand(repo); | |||
} | |||
/** | |||
* Get repository | |||
* |
@@ -0,0 +1,46 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.api; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier; | |||
import org.eclipse.jgit.revwalk.RevObject; | |||
/** | |||
* A {@code VerificationResult} describes the outcome of a signature | |||
* verification. | |||
* | |||
* @see VerifySignatureCommand | |||
* | |||
* @since 5.11 | |||
*/ | |||
public interface VerificationResult { | |||
/** | |||
* If an error occurred during signature verification, this retrieves the | |||
* exception. | |||
* | |||
* @return the exception, or {@code null} if none occurred | |||
*/ | |||
Throwable getException(); | |||
/** | |||
* Retrieves the signature verification result. | |||
* | |||
* @return the result, or {@code null} if none was computed | |||
*/ | |||
GpgSignatureVerifier.SignatureVerification getVerification(); | |||
/** | |||
* Retrieves the git object of which the signature was verified. | |||
* | |||
* @return the git object | |||
*/ | |||
RevObject getObject(); | |||
} |
@@ -0,0 +1,307 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.api; | |||
import java.io.IOException; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.eclipse.jgit.annotations.NonNull; | |||
import org.eclipse.jgit.api.errors.JGitInternalException; | |||
import org.eclipse.jgit.api.errors.ServiceUnavailableException; | |||
import org.eclipse.jgit.api.errors.WrongObjectTypeException; | |||
import org.eclipse.jgit.errors.MissingObjectException; | |||
import org.eclipse.jgit.internal.JGitText; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.GpgConfig; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.Repository; | |||
import org.eclipse.jgit.revwalk.RevObject; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
/** | |||
* A command to verify GPG signatures on tags or commits. | |||
* | |||
* @since 5.11 | |||
*/ | |||
public class VerifySignatureCommand extends GitCommand<Map<String, VerificationResult>> { | |||
/** | |||
* Describes what kind of objects shall be handled by a | |||
* {@link VerifySignatureCommand}. | |||
*/ | |||
public enum VerifyMode { | |||
/** | |||
* Handle any object type, ignore anything that is not a commit or tag. | |||
*/ | |||
ANY, | |||
/** | |||
* Handle only commits; throw a {@link WrongObjectTypeException} for | |||
* anything else. | |||
*/ | |||
COMMITS, | |||
/** | |||
* Handle only tags; throw a {@link WrongObjectTypeException} for | |||
* anything else. | |||
*/ | |||
TAGS | |||
} | |||
private final Set<String> namesToCheck = new HashSet<>(); | |||
private VerifyMode mode = VerifyMode.ANY; | |||
private GpgSignatureVerifier verifier; | |||
private GpgConfig config; | |||
private boolean ownVerifier; | |||
/** | |||
* Creates a new {@link VerifySignatureCommand} for the given {@link Repository}. | |||
* | |||
* @param repo | |||
* to operate on | |||
*/ | |||
public VerifySignatureCommand(Repository repo) { | |||
super(repo); | |||
} | |||
/** | |||
* Add a name of an object (SHA-1, ref name; anything that can be | |||
* {@link Repository#resolve(String) resolved}) to the command to have its | |||
* signature verified. | |||
* | |||
* @param name | |||
* to add | |||
* @return {@code this} | |||
*/ | |||
public VerifySignatureCommand addName(String name) { | |||
checkCallable(); | |||
namesToCheck.add(name); | |||
return this; | |||
} | |||
/** | |||
* Add names of objects (SHA-1, ref name; anything that can be | |||
* {@link Repository#resolve(String) resolved}) to the command to have their | |||
* signatures verified. | |||
* | |||
* @param names | |||
* to add; duplicates will be ignored | |||
* @return {@code this} | |||
*/ | |||
public VerifySignatureCommand addNames(String... names) { | |||
checkCallable(); | |||
namesToCheck.addAll(Arrays.asList(names)); | |||
return this; | |||
} | |||
/** | |||
* Add names of objects (SHA-1, ref name; anything that can be | |||
* {@link Repository#resolve(String) resolved}) to the command to have their | |||
* signatures verified. | |||
* | |||
* @param names | |||
* to add; duplicates will be ignored | |||
* @return {@code this} | |||
*/ | |||
public VerifySignatureCommand addNames(Collection<String> names) { | |||
checkCallable(); | |||
namesToCheck.addAll(names); | |||
return this; | |||
} | |||
/** | |||
* Sets the mode of operation for this command. | |||
* | |||
* @param mode | |||
* the {@link VerifyMode} to set | |||
* @return {@code this} | |||
*/ | |||
public VerifySignatureCommand setMode(@NonNull VerifyMode mode) { | |||
checkCallable(); | |||
this.mode = mode; | |||
return this; | |||
} | |||
/** | |||
* Sets the {@link GpgSignatureVerifier} to use. | |||
* | |||
* @param verifier | |||
* the {@link GpgSignatureVerifier} to use, or {@code null} to | |||
* use the default verifier | |||
* @return {@code this} | |||
*/ | |||
public VerifySignatureCommand setVerifier(GpgSignatureVerifier verifier) { | |||
checkCallable(); | |||
this.verifier = verifier; | |||
return this; | |||
} | |||
/** | |||
* Sets an external {@link GpgConfig} to use. Whether it will be used it at | |||
* the discretion of the {@link #setVerifier(GpgSignatureVerifier)}. | |||
* | |||
* @param config | |||
* to set; if {@code null}, the config will be loaded from the | |||
* git config of the repository | |||
* @return {@code this} | |||
* @since 5.11 | |||
*/ | |||
public VerifySignatureCommand setGpgConfig(GpgConfig config) { | |||
checkCallable(); | |||
this.config = config; | |||
return this; | |||
} | |||
/** | |||
* Retrieves the currently set {@link GpgSignatureVerifier}. Can be used | |||
* after a successful {@link #call()} to get the verifier that was used. | |||
* | |||
* @return the {@link GpgSignatureVerifier} | |||
*/ | |||
public GpgSignatureVerifier getVerifier() { | |||
return verifier; | |||
} | |||
/** | |||
* {@link Repository#resolve(String) Resolves} all names added to the | |||
* command to git objects and verifies their signature. Non-existing objects | |||
* are ignored. | |||
* <p> | |||
* Depending on the {@link #setMode(VerifyMode)}, only tags or commits or | |||
* any kind of objects are allowed. | |||
* </p> | |||
* <p> | |||
* Unsigned objects are silently skipped. | |||
* </p> | |||
* | |||
* @return a map of the given names to the corresponding | |||
* {@link VerificationResult}, excluding ignored or skipped objects. | |||
* @throws ServiceUnavailableException | |||
* if no {@link GpgSignatureVerifier} was set and no | |||
* {@link GpgSignatureVerifierFactory} is available | |||
* @throws WrongObjectTypeException | |||
* if a name resolves to an object of a type not allowed by the | |||
* {@link #setMode(VerifyMode)} mode | |||
*/ | |||
@Override | |||
@NonNull | |||
public Map<String, VerificationResult> call() | |||
throws ServiceUnavailableException, WrongObjectTypeException { | |||
checkCallable(); | |||
setCallable(false); | |||
Map<String, VerificationResult> result = new HashMap<>(); | |||
if (verifier == null) { | |||
GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory | |||
.getDefault(); | |||
if (factory == null) { | |||
throw new ServiceUnavailableException( | |||
JGitText.get().signatureVerificationUnavailable); | |||
} | |||
verifier = factory.getVerifier(); | |||
ownVerifier = true; | |||
} | |||
if (config == null) { | |||
config = new GpgConfig(repo.getConfig()); | |||
} | |||
try (RevWalk walk = new RevWalk(repo)) { | |||
for (String toCheck : namesToCheck) { | |||
ObjectId id = repo.resolve(toCheck); | |||
if (id != null && !ObjectId.zeroId().equals(id)) { | |||
RevObject object; | |||
try { | |||
object = walk.parseAny(id); | |||
} catch (MissingObjectException e) { | |||
continue; | |||
} | |||
VerificationResult verification = verifyOne(object); | |||
if (verification != null) { | |||
result.put(toCheck, verification); | |||
} | |||
} | |||
} | |||
} catch (IOException e) { | |||
throw new JGitInternalException( | |||
JGitText.get().signatureVerificationError, e); | |||
} finally { | |||
if (ownVerifier) { | |||
verifier.clear(); | |||
} | |||
} | |||
return result; | |||
} | |||
private VerificationResult verifyOne(RevObject object) | |||
throws WrongObjectTypeException, IOException { | |||
int type = object.getType(); | |||
if (VerifyMode.TAGS.equals(mode) && type != Constants.OBJ_TAG) { | |||
throw new WrongObjectTypeException(object, Constants.OBJ_TAG); | |||
} else if (VerifyMode.COMMITS.equals(mode) | |||
&& type != Constants.OBJ_COMMIT) { | |||
throw new WrongObjectTypeException(object, Constants.OBJ_COMMIT); | |||
} | |||
if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) { | |||
try { | |||
GpgSignatureVerifier.SignatureVerification verification = verifier | |||
.verifySignature(object, config); | |||
if (verification == null) { | |||
// Not signed | |||
return null; | |||
} | |||
// Create new result | |||
return new Result(object, verification, null); | |||
} catch (JGitInternalException e) { | |||
return new Result(object, null, e); | |||
} | |||
} | |||
return null; | |||
} | |||
private static class Result implements VerificationResult { | |||
private final Throwable throwable; | |||
private final SignatureVerification verification; | |||
private final RevObject object; | |||
public Result(RevObject object, SignatureVerification verification, | |||
Throwable throwable) { | |||
this.object = object; | |||
this.verification = verification; | |||
this.throwable = throwable; | |||
} | |||
@Override | |||
public Throwable getException() { | |||
return throwable; | |||
} | |||
@Override | |||
public SignatureVerification getVerification() { | |||
return verification; | |||
} | |||
@Override | |||
public RevObject getObject() { | |||
return object; | |||
} | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.api.errors; | |||
import java.text.MessageFormat; | |||
import org.eclipse.jgit.internal.JGitText; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
/** | |||
* A given object is not of an expected object type. | |||
* | |||
* @since 5.11 | |||
*/ | |||
public class WrongObjectTypeException extends GitAPIException { | |||
private static final long serialVersionUID = 1L; | |||
private String name; | |||
private int type; | |||
/** | |||
* Construct a {@link WrongObjectTypeException} for the specified object id, | |||
* giving the expected type. | |||
* | |||
* @param id | |||
* {@link ObjectId} of the object with the unexpected type | |||
* @param type | |||
* expected object type code; see | |||
* {@link Constants}{@code .OBJ_*}. | |||
*/ | |||
public WrongObjectTypeException(ObjectId id, int type) { | |||
super(MessageFormat.format(JGitText.get().objectIsNotA, id.name(), | |||
Constants.typeString(type))); | |||
this.name = id.name(); | |||
this.type = type; | |||
} | |||
/** | |||
* Retrieves the name (SHA-1) of the object. | |||
* | |||
* @return the name | |||
*/ | |||
public String getObjectId() { | |||
return name; | |||
} | |||
/** | |||
* Retrieves the expected type code. See {@link Constants}{@code .OBJ_*}. | |||
* | |||
* @return the type code | |||
*/ | |||
public int getExpectedType() { | |||
return type; | |||
} | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com> | |||
* Copyright (C) 2012, Research In Motion Limited and others | |||
* Copyright (C) 2012, 2021 Research In Motion Limited 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 | |||
@@ -650,6 +650,8 @@ public class JGitText extends TranslationBundle { | |||
/***/ public String shortReadOfBlock; | |||
/***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes; | |||
/***/ public String shortSkipOfBlock; | |||
/***/ public String signatureVerificationError; | |||
/***/ public String signatureVerificationUnavailable; | |||
/***/ public String signedTagMessageNoLf; | |||
/***/ public String signingServiceUnavailable; | |||
/***/ public String similarityScoreMustBeWithinBounds; | |||
@@ -791,6 +793,13 @@ public class JGitText extends TranslationBundle { | |||
/***/ public String URINotSupported; | |||
/***/ public String userConfigInvalid; | |||
/***/ public String validatingGitModules; | |||
/***/ public String verifySignatureBad; | |||
/***/ public String verifySignatureExpired; | |||
/***/ public String verifySignatureGood; | |||
/***/ public String verifySignatureIssuer; | |||
/***/ public String verifySignatureKey; | |||
/***/ public String verifySignatureMade; | |||
/***/ public String verifySignatureTrust; | |||
/***/ public String walkFailure; | |||
/***/ public String wantNoSpaceWithCapabilities; | |||
/***/ public String wantNotValid; |
@@ -0,0 +1,158 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.lib; | |||
import java.io.IOException; | |||
import java.util.Date; | |||
import org.eclipse.jgit.annotations.NonNull; | |||
import org.eclipse.jgit.annotations.Nullable; | |||
import org.eclipse.jgit.api.errors.JGitInternalException; | |||
import org.eclipse.jgit.revwalk.RevObject; | |||
/** | |||
* A {@code GpgVerifier} can verify GPG signatures on git commits and tags. | |||
* | |||
* @since 5.11 | |||
*/ | |||
public interface GpgSignatureVerifier { | |||
/** | |||
* Verifies the signature on a signed commit or tag. | |||
* | |||
* @param object | |||
* to verify | |||
* @param config | |||
* the {@link GpgConfig} to use | |||
* @return a {@link SignatureVerification} describing the outcome of the | |||
* verification, or {@code null} if the object was not signed | |||
* @throws IOException | |||
* if an error occurs getting a public key | |||
* @throws org.eclipse.jgit.api.errors.JGitInternalException | |||
* if signature verification fails | |||
*/ | |||
@Nullable | |||
SignatureVerification verifySignature(@NonNull RevObject object, | |||
@NonNull GpgConfig config) throws IOException; | |||
/** | |||
* Verifies a given signature for given data. | |||
* | |||
* @param data | |||
* the signature is for | |||
* @param signatureData | |||
* the ASCII-armored signature | |||
* @return a {@link SignatureVerification} describing the outcome | |||
* @throws IOException | |||
* if the signature cannot be parsed | |||
* @throws JGitInternalException | |||
* if signature verification fails | |||
*/ | |||
public SignatureVerification verify(byte[] data, byte[] signatureData) | |||
throws IOException; | |||
/** | |||
* Retrieves the name of this verifier. This should be a short string | |||
* identifying the engine that verified the signature, like "gpg" if GPG is | |||
* used, or "bc" for a BouncyCastle implementation. | |||
* | |||
* @return the name | |||
*/ | |||
@NonNull | |||
String getName(); | |||
/** | |||
* A {@link GpgSignatureVerifier} may cache public keys to speed up | |||
* verifying signatures on multiple objects. This clears this cache, if any. | |||
*/ | |||
void clear(); | |||
/** | |||
* A {@code SignatureVerification} returns data about a (positively or | |||
* negatively) verified signature. | |||
*/ | |||
interface SignatureVerification { | |||
// Data about the signature. | |||
@NonNull | |||
Date getCreationDate(); | |||
// Data from the signature used to find a public key. | |||
/** | |||
* Obtains the signer as stored in the signature, if known. | |||
* | |||
* @return the signer, or {@code null} if unknown | |||
*/ | |||
String getSigner(); | |||
/** | |||
* Obtains the short or long fingerprint of the public key as stored in | |||
* the signature, if known. | |||
* | |||
* @return the fingerprint, or {@code null} if unknown | |||
*/ | |||
String getKeyFingerprint(); | |||
// Some information about the found public key. | |||
/** | |||
* Obtains the OpenPGP user ID associated with the key. | |||
* | |||
* @return the user id, or {@code null} if unknown | |||
*/ | |||
String getKeyUser(); | |||
/** | |||
* Tells whether the public key used for this signature verification was | |||
* expired when the signature was created. | |||
* | |||
* @return {@code true} if the key was expired already, {@code false} | |||
* otherwise | |||
*/ | |||
boolean isExpired(); | |||
/** | |||
* Obtains the trust level of the public key used to verify the | |||
* signature. | |||
* | |||
* @return the trust level | |||
*/ | |||
@NonNull | |||
TrustLevel getTrustLevel(); | |||
// The verification result. | |||
/** | |||
* Tells whether the signature verification was successful. | |||
* | |||
* @return {@code true} if the signature was verified successfully; | |||
* {@code false} if not. | |||
*/ | |||
boolean getVerified(); | |||
/** | |||
* Obtains a human-readable message giving additional information about | |||
* the outcome of the verification. | |||
* | |||
* @return the message, or {@code null} if none set. | |||
*/ | |||
String getMessage(); | |||
} | |||
/** | |||
* The owner's trust in a public key. | |||
*/ | |||
enum TrustLevel { | |||
UNKNOWN, NEVER, MARGINAL, FULL, ULTIMATE | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.lib; | |||
import java.util.Iterator; | |||
import java.util.ServiceConfigurationError; | |||
import java.util.ServiceLoader; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
/** | |||
* A {@code GpgSignatureVerifierFactory} creates {@link GpgSignatureVerifier} instances. | |||
* | |||
* @since 5.11 | |||
*/ | |||
public abstract class GpgSignatureVerifierFactory { | |||
private static final Logger LOG = LoggerFactory | |||
.getLogger(GpgSignatureVerifierFactory.class); | |||
private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault(); | |||
private static GpgSignatureVerifierFactory loadDefault() { | |||
try { | |||
ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader | |||
.load(GpgSignatureVerifierFactory.class); | |||
Iterator<GpgSignatureVerifierFactory> iter = loader.iterator(); | |||
if (iter.hasNext()) { | |||
return iter.next(); | |||
} | |||
} catch (ServiceConfigurationError e) { | |||
LOG.error(e.getMessage(), e); | |||
} | |||
return null; | |||
} | |||
/** | |||
* Retrieves the default factory. | |||
* | |||
* @return the default factory or {@code null} if none set | |||
*/ | |||
public static GpgSignatureVerifierFactory getDefault() { | |||
return defaultFactory; | |||
} | |||
/** | |||
* Sets the default factory. | |||
* | |||
* @param factory | |||
* the new default factory | |||
*/ | |||
public static void setDefault(GpgSignatureVerifierFactory factory) { | |||
defaultFactory = factory; | |||
} | |||
/** | |||
* Creates a new {@link GpgSignatureVerifier}. | |||
* | |||
* @return the new {@link GpgSignatureVerifier} | |||
*/ | |||
public abstract GpgSignatureVerifier getVerifier(); | |||
} |
@@ -1,7 +1,7 @@ | |||
/* | |||
* Copyright (C) 2008-2009, Google Inc. | |||
* Copyright (C) 2008, 2009, Google Inc. | |||
* Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> | |||
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others | |||
* Copyright (C) 2008, 2021, Shawn O. Pearce <spearce@spearce.org> 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 | |||
@@ -343,6 +343,22 @@ public class RevTag extends RevObject { | |||
return tagName; | |||
} | |||
/** | |||
* Obtain the raw unparsed tag body (<b>NOTE - THIS IS NOT A COPY</b>). | |||
* <p> | |||
* This method is exposed only to provide very fast, efficient access to | |||
* this tag's message buffer. Applications relying on this buffer should be | |||
* very careful to ensure they do not modify its contents during their use | |||
* of it. | |||
* | |||
* @return the raw unparsed tag body. This is <b>NOT A COPY</b>. Do not | |||
* alter the returned array. | |||
* @since 5.11 | |||
*/ | |||
public final byte[] getRawBuffer() { | |||
return buffer; | |||
} | |||
/** | |||
* Discard the message buffer to reduce memory usage. | |||
* <p> |
@@ -0,0 +1,86 @@ | |||
/* | |||
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.util; | |||
import java.text.MessageFormat; | |||
import java.util.Locale; | |||
import org.eclipse.jgit.internal.JGitText; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; | |||
import org.eclipse.jgit.lib.GpgSignatureVerifier.TrustLevel; | |||
import org.eclipse.jgit.lib.PersonIdent; | |||
/** | |||
* Utilities for signature verification. | |||
* | |||
* @since 5.11 | |||
*/ | |||
public final class SignatureUtils { | |||
private SignatureUtils() { | |||
// No instantiation | |||
} | |||
/** | |||
* Writes information about a signature verification to a string. | |||
* | |||
* @param verification | |||
* to show | |||
* @param creator | |||
* of the object verified; used for time zone information | |||
* @param formatter | |||
* to use for dates | |||
* @return a textual representation of the {@link SignatureVerification}, | |||
* using LF as line separator | |||
*/ | |||
public static String toString(SignatureVerification verification, | |||
PersonIdent creator, GitDateFormatter formatter) { | |||
StringBuilder result = new StringBuilder(); | |||
// Use the creator's timezone for the signature date | |||
PersonIdent dateId = new PersonIdent(creator, | |||
verification.getCreationDate()); | |||
result.append(MessageFormat.format(JGitText.get().verifySignatureMade, | |||
formatter.formatDate(dateId))); | |||
result.append('\n'); | |||
result.append(MessageFormat.format( | |||
JGitText.get().verifySignatureKey, | |||
verification.getKeyFingerprint().toUpperCase(Locale.ROOT))); | |||
result.append('\n'); | |||
if (!StringUtils.isEmptyOrNull(verification.getSigner())) { | |||
result.append( | |||
MessageFormat.format(JGitText.get().verifySignatureIssuer, | |||
verification.getSigner())); | |||
result.append('\n'); | |||
} | |||
String msg; | |||
if (verification.getVerified()) { | |||
if (verification.isExpired()) { | |||
msg = JGitText.get().verifySignatureExpired; | |||
} else { | |||
msg = JGitText.get().verifySignatureGood; | |||
} | |||
} else { | |||
msg = JGitText.get().verifySignatureBad; | |||
} | |||
result.append(MessageFormat.format(msg, verification.getKeyUser())); | |||
if (!TrustLevel.UNKNOWN.equals(verification.getTrustLevel())) { | |||
result.append(' ' + MessageFormat | |||
.format(JGitText.get().verifySignatureTrust, verification | |||
.getTrustLevel().name().toLowerCase(Locale.ROOT))); | |||
} | |||
result.append('\n'); | |||
msg = verification.getMessage(); | |||
if (!StringUtils.isEmptyOrNull(msg)) { | |||
result.append(msg); | |||
result.append('\n'); | |||
} | |||
return result.toString(); | |||
} | |||
} |