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
Bundle-Version: 5.11.0.qualifier | Bundle-Version: 5.11.0.qualifier | ||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 | Bundle-RequiredExecutionEnvironment: JavaSE-1.8 | ||||
Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)", | 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;version="[1.65.0,2.0.0)", | ||||
org.bouncycastle.gpg.keybox;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.gpg.keybox.jcajce;version="[1.65.0,2.0.0)", | ||||
org.bouncycastle.jce.provider;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;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;version="[1.65.0,2.0.0)", | ||||
org.bouncycastle.openpgp.operator.jcajce;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)", | org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)", |
org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory |
gpgNotASigningKey=Secret key ({0}) is not suitable for signing | gpgNotASigningKey=Secret key ({0}) is not suitable for signing | ||||
gpgKeyInfo=GPG Key (fingerprint {0}) | gpgKeyInfo=GPG Key (fingerprint {0}) | ||||
gpgSigningCancelled=Signing was cancelled | 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. | unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available. |
/* | |||||
* 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; | package org.eclipse.jgit.gpg.bc.internal; | ||||
import org.eclipse.jgit.nls.NLS; | import org.eclipse.jgit.nls.NLS; | ||||
/***/ public String gpgNotASigningKey; | /***/ public String gpgNotASigningKey; | ||||
/***/ public String gpgKeyInfo; | /***/ public String gpgKeyInfo; | ||||
/***/ public String gpgSigningCancelled; | /***/ 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; | /***/ public String unableToSignCommitNoSecretKey; | ||||
} | } |
/* | /* | ||||
* 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import java.io.BufferedInputStream; | import java.io.BufferedInputStream; | ||||
import java.io.File; | import java.io.File; | ||||
import java.io.FileNotFoundException; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.net.URISyntaxException; | import java.net.URISyntaxException; | ||||
import java.nio.file.DirectoryStream; | import java.nio.file.DirectoryStream; | ||||
import java.nio.file.Files; | import java.nio.file.Files; | ||||
import java.nio.file.InvalidPathException; | import java.nio.file.InvalidPathException; | ||||
import java.nio.file.NoSuchFileException; | |||||
import java.nio.file.Path; | import java.nio.file.Path; | ||||
import java.nio.file.Paths; | import java.nio.file.Paths; | ||||
import java.security.NoSuchAlgorithmException; | import java.security.NoSuchAlgorithmException; | ||||
return false; | return false; | ||||
} | } | ||||
private String toFingerprint(String keyId) { | |||||
private static String toFingerprint(String keyId) { | |||||
if (keyId.startsWith("0x")) { //$NON-NLS-1$ | if (keyId.startsWith("0x")) { //$NON-NLS-1$ | ||||
return keyId.substring(2); | return keyId.substring(2); | ||||
} | } | ||||
return keyId; | 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 { | throws IOException { | ||||
String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); | |||||
if (keyId.isEmpty()) { | if (keyId.isEmpty()) { | ||||
return null; | return null; | ||||
} | } | ||||
return null; | return null; | ||||
} | } | ||||
private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob) | |||||
private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob, | |||||
String keySpec) | |||||
throws IOException { | throws IOException { | ||||
for (UserID userID : keyBlob.getUserIds()) { | for (UserID userID : keyBlob.getUserIds()) { | ||||
if (containsSigningKey(userID.getUserIDAsString(), signingKey)) { | |||||
if (containsSigningKey(userID.getUserIDAsString(), keySpec)) { | |||||
return getSigningPublicKey(keyBlob); | return getSigningPublicKey(keyBlob); | ||||
} | } | ||||
} | } | ||||
* | * | ||||
* @param keyboxFile | * @param keyboxFile | ||||
* the KeyBox file | * 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>) | * @return publicKey the public key (maybe <code>null</code>) | ||||
* @throws IOException | * @throws IOException | ||||
* in case of problems reading the file | * in case of problems reading the file | ||||
* @throws NoOpenPgpKeyException | * @throws NoOpenPgpKeyException | ||||
* if the file does not contain any OpenPGP key | * 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, | throws IOException, NoSuchAlgorithmException, | ||||
NoSuchProviderException, NoOpenPgpKeyException { | NoSuchProviderException, NoOpenPgpKeyException { | ||||
KeyBox keyBox = readKeyBoxFile(keyboxFile); | KeyBox keyBox = readKeyBoxFile(keyboxFile); | ||||
String id = keyId != null ? keyId | |||||
: toFingerprint(keySpec).toLowerCase(Locale.ROOT); | |||||
boolean hasOpenPgpKey = false; | boolean hasOpenPgpKey = false; | ||||
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) { | for (KeyBlob keyBlob : keyBox.getKeyBlobs()) { | ||||
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) { | if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) { | ||||
hasOpenPgpKey = true; | hasOpenPgpKey = true; | ||||
PGPPublicKey key = findPublicKeyByKeyId(keyBlob); | |||||
PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id); | |||||
if (key != null) { | if (key != null) { | ||||
return key; | return key; | ||||
} | } | ||||
key = findPublicKeyByUserId(keyBlob); | |||||
key = findPublicKeyByUserId(keyBlob, keySpec); | |||||
if (key != null) { | if (key != null) { | ||||
return key; | return key; | ||||
} | } | ||||
// pubring.gpg also try secring.gpg to find the secret key. | // pubring.gpg also try secring.gpg to find the secret key. | ||||
if (exists(USER_KEYBOX_PATH)) { | if (exists(USER_KEYBOX_PATH)) { | ||||
try { | try { | ||||
publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH); | |||||
publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null, | |||||
signingKey); | |||||
if (publicKey != null) { | if (publicKey != null) { | ||||
key = findSecretKeyForKeyBoxPublicKey(publicKey, | key = findSecretKeyForKeyBoxPublicKey(publicKey, | ||||
USER_KEYBOX_PATH); | USER_KEYBOX_PATH); | ||||
} | } | ||||
} | } | ||||
if (exists(USER_PGP_PUBRING_FILE)) { | if (exists(USER_PGP_PUBRING_FILE)) { | ||||
publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE); | |||||
publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE, null, | |||||
signingKey); | |||||
if (publicKey != null) { | if (publicKey != null) { | ||||
// GPG < 2.1 may have both; the agent using the directory | // GPG < 2.1 may have both; the agent using the directory | ||||
// and gpg using secring.gpg. GPG >= 2.1 delegates all | // and gpg using secring.gpg. GPG >= 2.1 delegates all | ||||
* Return the first public key matching the key id ({@link #signingKey}. | * Return the first public key matching the key id ({@link #signingKey}. | ||||
* | * | ||||
* @param pubringFile | * @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 | * @return the PGP public key, or {@code null} if none found | ||||
* @throws IOException | * @throws IOException | ||||
* @throws PGPException | * @throws PGPException | ||||
* on BouncyCastle errors | * on BouncyCastle errors | ||||
*/ | */ | ||||
private PGPPublicKey findPublicKeyInPubring(Path pubringFile) | |||||
private static PGPPublicKey findPublicKeyInPubring(Path pubringFile, | |||||
String keyId, String keySpec) | |||||
throws IOException, PGPException { | throws IOException, PGPException { | ||||
try (InputStream in = newInputStream(pubringFile)) { | try (InputStream in = newInputStream(pubringFile)) { | ||||
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection( | PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection( | ||||
new BufferedInputStream(in), | new BufferedInputStream(in), | ||||
new JcaKeyFingerprintCalculator()); | new JcaKeyFingerprintCalculator()); | ||||
String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); | |||||
String id = keyId != null ? keyId | |||||
: toFingerprint(keySpec).toLowerCase(Locale.ROOT); | |||||
Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings(); | Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings(); | ||||
while (keyrings.hasNext()) { | while (keyrings.hasNext()) { | ||||
PGPPublicKeyRing keyRing = keyrings.next(); | PGPPublicKeyRing keyRing = keyrings.next(); | ||||
// try key id | // try key id | ||||
String fingerprint = Hex.toHexString(key.getFingerprint()) | String fingerprint = Hex.toHexString(key.getFingerprint()) | ||||
.toLowerCase(Locale.ROOT); | .toLowerCase(Locale.ROOT); | ||||
if (fingerprint.endsWith(keyId)) { | |||||
if (fingerprint.endsWith(id)) { | |||||
return key; | return key; | ||||
} | } | ||||
// try user id | // try user id | ||||
Iterator<String> userIDs = key.getUserIDs(); | Iterator<String> userIDs = key.getUserIDs(); | ||||
while (userIDs.hasNext()) { | while (userIDs.hasNext()) { | ||||
String userId = userIDs.next(); | String userId = userIDs.next(); | ||||
if (containsSigningKey(userId, signingKey)) { | |||||
if (containsSigningKey(userId, keySpec)) { | |||||
return key; | return key; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} catch (FileNotFoundException | NoSuchFileException e) { | |||||
// Ignore and return null | |||||
} | } | ||||
return null; | return null; | ||||
} | } | ||||
private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint) | |||||
private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint) | |||||
throws IOException { | throws IOException { | ||||
return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing() | return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing() | ||||
.getPublicKey(fingerprint); | .getPublicKey(fingerprint); | ||||
} | } | ||||
private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException { | |||||
private static PGPPublicKey getSigningPublicKey(KeyBlob blob) | |||||
throws IOException { | |||||
PGPPublicKey masterKey = null; | PGPPublicKey masterKey = null; | ||||
Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob) | Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob) | ||||
.getPGPPublicKeyRing().getPublicKeys(); | .getPGPPublicKeyRing().getPublicKeys(); | ||||
return masterKey; | return masterKey; | ||||
} | } | ||||
private boolean isSigningKey(PGPPublicKey key) { | |||||
private static boolean isSigningKey(PGPPublicKey key) { | |||||
Iterator signatures = key.getSignatures(); | Iterator signatures = key.getSignatures(); | ||||
while (signatures.hasNext()) { | while (signatures.hasNext()) { | ||||
PGPSignature sig = (PGPSignature) signatures.next(); | PGPSignature sig = (PGPSignature) signatures.next(); | ||||
return false; | return false; | ||||
} | } | ||||
private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException, | |||||
private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException, | |||||
NoSuchAlgorithmException, NoSuchProviderException, | NoSuchAlgorithmException, NoSuchProviderException, | ||||
NoOpenPgpKeyException { | NoOpenPgpKeyException { | ||||
if (keyboxFile.toFile().length() == 0) { | if (keyboxFile.toFile().length() == 0) { |
/* | |||||
* 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; | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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(); | |||||
} | |||||
} |
/* | /* | ||||
* 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.internal.JGitText; | import org.eclipse.jgit.internal.JGitText; | ||||
import org.eclipse.jgit.lib.CommitBuilder; | import org.eclipse.jgit.lib.CommitBuilder; | ||||
import org.eclipse.jgit.lib.GpgConfig; | import org.eclipse.jgit.lib.GpgConfig; | ||||
import org.eclipse.jgit.lib.GpgObjectSigner; | |||||
import org.eclipse.jgit.lib.GpgSignature; | import org.eclipse.jgit.lib.GpgSignature; | ||||
import org.eclipse.jgit.lib.GpgSigner; | import org.eclipse.jgit.lib.GpgSigner; | ||||
import org.eclipse.jgit.lib.GpgObjectSigner; | |||||
import org.eclipse.jgit.lib.ObjectBuilder; | import org.eclipse.jgit.lib.ObjectBuilder; | ||||
import org.eclipse.jgit.lib.PersonIdent; | import org.eclipse.jgit.lib.PersonIdent; | ||||
import org.eclipse.jgit.lib.GpgConfig.GpgFormat; | import org.eclipse.jgit.lib.GpgConfig.GpgFormat; | ||||
} | } | ||||
} | } | ||||
private String extractSignerId(String pgpUserId) { | |||||
static String extractSignerId(String pgpUserId) { | |||||
int from = pgpUserId.indexOf('<'); | int from = pgpUserId.indexOf('<'); | ||||
if (from >= 0) { | if (from >= 0) { | ||||
int to = pgpUserId.indexOf('>', from + 1); | int to = pgpUserId.indexOf('>', from + 1); |
invalidRecurseSubmodulesMode=Invalid recurse submodules mode: {0} | invalidRecurseSubmodulesMode=Invalid recurse submodules mode: {0} | ||||
invalidUntrackedFilesMode=Invalid untracked files mode ''{0}'' | invalidUntrackedFilesMode=Invalid untracked files mode ''{0}'' | ||||
jgitVersion=jgit version {0} | jgitVersion=jgit version {0} | ||||
lineFormat={0} | |||||
listeningOn=Listening on {0} | |||||
lfsNoAccessKey=No accessKey in {0} | lfsNoAccessKey=No accessKey in {0} | ||||
lfsNoSecretKey=No secretKey in {0} | lfsNoSecretKey=No secretKey in {0} | ||||
lfsProtocolUrl=LFS protocol URL: {0} | lfsProtocolUrl=LFS protocol URL: {0} | ||||
lfsStoreDirectory=LFS objects stored in: {0} | lfsStoreDirectory=LFS objects stored in: {0} | ||||
lfsStoreUrl=LFS store URL: {0} | lfsStoreUrl=LFS store URL: {0} | ||||
lfsUnknownStoreType="Unknown LFS store type: {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} | mergeConflict=CONFLICT(content): Merge conflict in {0} | ||||
mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge: | 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 | mergeFailed=Automatic merge failed; fix conflicts and then commit the result | ||||
usage_showRefNamesMatchingCommits=Show ref names matching commits | usage_showRefNamesMatchingCommits=Show ref names matching commits | ||||
usage_showPatch=display patch | usage_showPatch=display patch | ||||
usage_showNotes=Add this ref to the list of note branches from which notes are displayed | 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_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_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/" | usage_srcPrefix=show the source prefix instead of "a/" | ||||
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_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_tagSign=create a signed annotated tag | ||||
usage_tagNoSign=suppress signing the tag | usage_tagNoSign=suppress signing the tag | ||||
usage_tagVerify=Verify the GPG signature | |||||
usage_untrackedFilesMode=show untracked files | usage_untrackedFilesMode=show untracked files | ||||
usage_updateRef=reference to update | usage_updateRef=reference to update | ||||
usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository | usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository |
/* | /* | ||||
* Copyright (C) 2010, Google Inc. | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.errors.LargeObjectException; | import org.eclipse.jgit.errors.LargeObjectException; | ||||
import org.eclipse.jgit.lib.AnyObjectId; | import org.eclipse.jgit.lib.AnyObjectId; | ||||
import org.eclipse.jgit.lib.Constants; | 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.ObjectId; | ||||
import org.eclipse.jgit.lib.PersonIdent; | import org.eclipse.jgit.lib.PersonIdent; | ||||
import org.eclipse.jgit.lib.Ref; | import org.eclipse.jgit.lib.Ref; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.notes.NoteMap; | import org.eclipse.jgit.notes.NoteMap; | ||||
import org.eclipse.jgit.pgm.internal.CLIText; | 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.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevTree; | import org.eclipse.jgit.revwalk.RevTree; | ||||
import org.eclipse.jgit.util.GitDateFormatter; | import org.eclipse.jgit.util.GitDateFormatter; | ||||
additionalNoteRefs.add(notesRef); | additionalNoteRefs.add(notesRef); | ||||
} | } | ||||
@Option(name = "--show-signature", usage = "usage_showSignature") | |||||
private boolean showSignature; | |||||
@Option(name = "--date", usage = "usage_date") | @Option(name = "--date", usage = "usage_date") | ||||
void dateFormat(String date) { | void dateFormat(String date) { | ||||
if (date.toLowerCase(Locale.ROOT).equals(date)) | if (date.toLowerCase(Locale.ROOT).equals(date)) | ||||
// END -- Options shared with Diff | // END -- Options shared with Diff | ||||
private GpgSignatureVerifier verifier; | |||||
private GpgConfig config; | |||||
Log() { | Log() { | ||||
dateFormatter = new GitDateFormatter(Format.DEFAULT); | dateFormatter = new GitDateFormatter(Format.DEFAULT); | ||||
} | } | ||||
/** {@inheritDoc} */ | /** {@inheritDoc} */ | ||||
@Override | @Override | ||||
protected void run() { | protected void run() { | ||||
config = new GpgConfig(db.getConfig()); | |||||
diffFmt.setRepository(db); | diffFmt.setRepository(db); | ||||
try { | try { | ||||
diffFmt.setPathFilter(pathFilter); | diffFmt.setPathFilter(pathFilter); | ||||
throw die(e.getMessage(), e); | throw die(e.getMessage(), e); | ||||
} finally { | } finally { | ||||
diffFmt.close(); | diffFmt.close(); | ||||
if (verifier != null) { | |||||
verifier.clear(); | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
outw.println(); | outw.println(); | ||||
if (showSignature) { | |||||
showSignature(c); | |||||
} | |||||
final PersonIdent author = c.getAuthorIdent(); | final PersonIdent author = c.getAuthorIdent(); | ||||
outw.println(MessageFormat.format(CLIText.get().authorInfo, author.getName(), author.getEmailAddress())); | outw.println(MessageFormat.format(CLIText.get().authorInfo, author.getName(), author.getEmailAddress())); | ||||
outw.println(MessageFormat.format(CLIText.get().dateInfo, | outw.println(MessageFormat.format(CLIText.get().dateInfo, | ||||
outw.flush(); | 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 | * @param c | ||||
* @return <code>true</code> if at least one note was printed, | * @return <code>true</code> if at least one note was printed, |
import org.eclipse.jgit.errors.RevisionSyntaxException; | import org.eclipse.jgit.errors.RevisionSyntaxException; | ||||
import org.eclipse.jgit.lib.Constants; | import org.eclipse.jgit.lib.Constants; | ||||
import org.eclipse.jgit.lib.FileMode; | 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.ObjectId; | ||||
import org.eclipse.jgit.lib.PersonIdent; | import org.eclipse.jgit.lib.PersonIdent; | ||||
import org.eclipse.jgit.lib.Repository; | 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.CLIText; | ||||
import org.eclipse.jgit.pgm.internal.VerificationUtils; | |||||
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; | import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; | ||||
import org.eclipse.jgit.revwalk.RevCommit; | import org.eclipse.jgit.revwalk.RevCommit; | ||||
import org.eclipse.jgit.revwalk.RevObject; | import org.eclipse.jgit.revwalk.RevObject; | ||||
@Option(name = "--", metaVar = "metaVar_path", handler = PathTreeFilterHandler.class) | @Option(name = "--", metaVar = "metaVar_path", handler = PathTreeFilterHandler.class) | ||||
protected TreeFilter pathFilter = TreeFilter.ALL; | protected TreeFilter pathFilter = TreeFilter.ALL; | ||||
@Option(name = "--show-signature", usage = "usage_showSignature") | |||||
private boolean showSignature; | |||||
// BEGIN -- Options shared with Diff | // BEGIN -- Options shared with Diff | ||||
@Option(name = "-p", usage = "usage_showPatch") | @Option(name = "-p", usage = "usage_showPatch") | ||||
boolean showPatch; | boolean showPatch; | ||||
} | } | ||||
outw.println(); | 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(); | byte[] rawSignature = tag.getRawGpgSignature(); | ||||
if (rawSignature != null) { | 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) { | for (String s : lines) { | ||||
outw.println(s); | outw.println(s); | ||||
} | } | ||||
c.getId().copyTo(outbuffer, outw); | c.getId().copyTo(outbuffer, outw); | ||||
outw.println(); | outw.println(); | ||||
if (showSignature) { | |||||
showSignature(c); | |||||
} | |||||
final PersonIdent author = c.getAuthorIdent(); | final PersonIdent author = c.getAuthorIdent(); | ||||
outw.println(MessageFormat.format(CLIText.get().authorInfo, | outw.println(MessageFormat.format(CLIText.get().authorInfo, | ||||
author.getName(), author.getEmailAddress())); | author.getName(), author.getEmailAddress())); | ||||
} | } | ||||
outw.println(); | 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(); | |||||
} | |||||
} | |||||
} | } |
* Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org> | * 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.lists@dewire.com> | ||||
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
import org.eclipse.jgit.api.Git; | import org.eclipse.jgit.api.Git; | ||||
import org.eclipse.jgit.api.ListTagCommand; | import org.eclipse.jgit.api.ListTagCommand; | ||||
import org.eclipse.jgit.api.TagCommand; | 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.GitAPIException; | ||||
import org.eclipse.jgit.api.errors.RefAlreadyExistsException; | 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.ObjectId; | ||||
import org.eclipse.jgit.lib.Ref; | import org.eclipse.jgit.lib.Ref; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.pgm.internal.CLIText; | 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.eclipse.jgit.revwalk.RevWalk; | ||||
import org.kohsuke.args4j.Argument; | import org.kohsuke.args4j.Argument; | ||||
import org.kohsuke.args4j.Option; | import org.kohsuke.args4j.Option; | ||||
@Command(common = true, usage = "usage_CreateATag") | @Command(common = true, usage = "usage_CreateATag") | ||||
class Tag extends TextBuiltin { | class Tag extends TextBuiltin { | ||||
@Option(name = "-f", usage = "usage_forceReplacingAnExistingTag") | |||||
@Option(name = "--force", aliases = { "-f" }, forbids = { "--delete", | |||||
"--verify" }, usage = "usage_forceReplacingAnExistingTag") | |||||
private boolean force; | private boolean force; | ||||
@Option(name = "-d", usage = "usage_tagDelete") | |||||
@Option(name = "--delete", aliases = { "-d" }, forbids = { | |||||
"--verify" }, usage = "usage_tagDelete") | |||||
private boolean delete; | private boolean delete; | ||||
@Option(name = "--annotate", aliases = { | @Option(name = "--annotate", aliases = { | ||||
"-a" }, usage = "usage_tagAnnotated") | |||||
"-a" }, forbids = { "--delete", | |||||
"--verify" }, usage = "usage_tagAnnotated") | |||||
private boolean annotated; | 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; | private String message; | ||||
@Option(name = "--sign", aliases = { "-s" }, forbids = { | @Option(name = "--sign", aliases = { "-s" }, forbids = { | ||||
"--no-sign" }, usage = "usage_tagSign") | |||||
"--no-sign", "--delete", "--verify" }, usage = "usage_tagSign") | |||||
private boolean sign; | private boolean sign; | ||||
@Option(name = "--no-sign", usage = "usage_tagNoSign", forbids = { | @Option(name = "--no-sign", usage = "usage_tagNoSign", forbids = { | ||||
"--sign" }) | |||||
"--sign", "--delete", "--verify" }) | |||||
private boolean noSign; | private boolean noSign; | ||||
@Option(name = "--local-user", aliases = { | @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; | 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") | @Argument(index = 0, metaVar = "metaVar_name") | ||||
private String tagName; | private String tagName; | ||||
protected void run() { | protected void run() { | ||||
try (Git git = new Git(db)) { | try (Git git = new Git(db)) { | ||||
if (tagName != null) { | 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) | List<String> deletedTags = git.tagDelete().setTags(tagName) | ||||
.call(); | .call(); | ||||
if (deletedTags.isEmpty()) { | if (deletedTags.isEmpty()) { | ||||
throw die(e.getMessage(), e); | 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()); | |||||
} | |||||
} | } |
/* | /* | ||||
* Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com> | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
/***/ public String lfsUnknownStoreType; | /***/ public String lfsUnknownStoreType; | ||||
/***/ public String lineFormat; | /***/ public String lineFormat; | ||||
/***/ public String listeningOn; | /***/ public String listeningOn; | ||||
/***/ public String logNoSignatureVerifier; | |||||
/***/ public String mergeCheckoutConflict; | /***/ public String mergeCheckoutConflict; | ||||
/***/ public String mergeConflict; | /***/ public String mergeConflict; | ||||
/***/ public String mergeFailed; | /***/ public String mergeFailed; |
/* | |||||
* 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); | |||||
} | |||||
} | |||||
} |
shortReadOfBlock=Short read of block. | shortReadOfBlock=Short read of block. | ||||
shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section. | shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section. | ||||
shortSkipOfBlock=Short skip of block. | 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. | signedTagMessageNoLf=A non-empty message of a signed tag must end in LF. | ||||
signingServiceUnavailable=Signing service is not available | signingServiceUnavailable=Signing service is not available | ||||
similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100. | similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100. | ||||
URINotSupported=URI not supported: {0} | URINotSupported=URI not supported: {0} | ||||
userConfigInvalid=Git config in the user's home directory {0} is invalid {1} | userConfigInvalid=Git config in the user's home directory {0} is invalid {1} | ||||
validatingGitModules=Validating .gitmodules files | 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. | walkFailure=Walk failure. | ||||
wantNoSpaceWithCapabilities=No space between oid and first capability in first want line | wantNoSpaceWithCapabilities=No space between oid and first capability in first want line | ||||
wantNotValid=want {0} not valid | wantNotValid=want {0} not valid |
/* | /* | ||||
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
return new RemoteSetUrlCommand(repo); | 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 | * Get repository | ||||
* | * |
/* | |||||
* 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(); | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | /* | ||||
* Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com> | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
/***/ public String shortReadOfBlock; | /***/ public String shortReadOfBlock; | ||||
/***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes; | /***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes; | ||||
/***/ public String shortSkipOfBlock; | /***/ public String shortSkipOfBlock; | ||||
/***/ public String signatureVerificationError; | |||||
/***/ public String signatureVerificationUnavailable; | |||||
/***/ public String signedTagMessageNoLf; | /***/ public String signedTagMessageNoLf; | ||||
/***/ public String signingServiceUnavailable; | /***/ public String signingServiceUnavailable; | ||||
/***/ public String similarityScoreMustBeWithinBounds; | /***/ public String similarityScoreMustBeWithinBounds; | ||||
/***/ public String URINotSupported; | /***/ public String URINotSupported; | ||||
/***/ public String userConfigInvalid; | /***/ public String userConfigInvalid; | ||||
/***/ public String validatingGitModules; | /***/ 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 walkFailure; | ||||
/***/ public String wantNoSpaceWithCapabilities; | /***/ public String wantNoSpaceWithCapabilities; | ||||
/***/ public String wantNotValid; | /***/ public String wantNotValid; |
/* | |||||
* 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 | |||||
} | |||||
} |
/* | |||||
* 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(); | |||||
} |
/* | /* | ||||
* Copyright (C) 2008-2009, Google Inc. | |||||
* Copyright (C) 2008, 2009, Google Inc. | |||||
* Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> | * 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 | * This program and the accompanying materials are made available under the | ||||
* terms of the Eclipse Distribution License v. 1.0 which is available at | * terms of the Eclipse Distribution License v. 1.0 which is available at | ||||
return tagName; | 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. | * Discard the message buffer to reduce memory usage. | ||||
* <p> | * <p> |
/* | |||||
* 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(); | |||||
} | |||||
} |