GPG signature verification via BouncyCastle
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>
This commit is contained in:
parent
15a38e5b4f
commit
3774fcc848
|
@ -9,11 +9,13 @@ Bundle-Localization: plugin
|
||||||
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)",
|
||||||
|
|
|
@ -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
|
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.
|
||||||
|
|
|
@ -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;
|
package org.eclipse.jgit.gpg.bc.internal;
|
||||||
|
|
||||||
import org.eclipse.jgit.nls.NLS;
|
import org.eclipse.jgit.nls.NLS;
|
||||||
|
@ -28,6 +37,13 @@ public static BCText get() {
|
||||||
/***/ 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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* 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
|
||||||
|
@ -14,12 +14,14 @@
|
||||||
|
|
||||||
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;
|
||||||
|
@ -247,16 +249,32 @@ private static boolean containsIgnoreCase(String a, String b) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -270,10 +288,11 @@ private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,6 +304,10 @@ private PGPPublicKey findPublicKeyByUserId(KeyBlob 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
|
||||||
|
@ -293,19 +316,22 @@ private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
|
||||||
* @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;
|
||||||
}
|
}
|
||||||
|
@ -349,7 +375,8 @@ public BouncyCastleGpgKey findSecretKey() throws IOException,
|
||||||
// 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);
|
||||||
|
@ -372,7 +399,8 @@ public BouncyCastleGpgKey findSecretKey() throws IOException,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
|
@ -562,6 +590,11 @@ private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
|
||||||
* 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
|
||||||
|
@ -569,14 +602,16 @@ private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
|
||||||
* @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();
|
||||||
|
@ -586,30 +621,33 @@ private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
|
||||||
// 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();
|
||||||
|
@ -629,7 +667,7 @@ private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException {
|
||||||
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();
|
||||||
|
@ -641,7 +679,7 @@ private boolean isSigningKey(PGPPublicKey key) {
|
||||||
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) {
|
||||||
|
|
|
@ -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
|
* 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
|
||||||
|
@ -39,9 +39,9 @@
|
||||||
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;
|
||||||
|
@ -210,7 +210,7 @@ public void signObject(@NonNull ObjectBuilder object,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -77,14 +77,15 @@ invalidHttpProxyOnlyHttpSupported=Invalid http_proxy: {0}: Only http supported.
|
||||||
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
|
||||||
|
@ -411,6 +412,7 @@ usage_show=Display one commit
|
||||||
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/"
|
||||||
|
@ -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_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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2010, Google Inc.
|
* Copyright (C) 2010, Google Inc.
|
||||||
* Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
|
* Copyright (C) 2006, 2008, Robin Rosenberg <robin.rosenberg@dewire.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
|
||||||
|
@ -31,12 +31,17 @@
|
||||||
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;
|
||||||
|
@ -68,6 +73,9 @@ void addAdditionalNoteRef(String notesRef) {
|
||||||
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))
|
||||||
|
@ -147,6 +155,10 @@ void noPrefix(@SuppressWarnings("unused") boolean on) {
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
|
@ -161,6 +173,7 @@ protected void init(Repository repository, String gitDir) {
|
||||||
/** {@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);
|
||||||
|
@ -197,6 +210,9 @@ protected void run() {
|
||||||
throw die(e.getMessage(), e);
|
throw die(e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
diffFmt.close();
|
diffFmt.close();
|
||||||
|
if (verifier != null) {
|
||||||
|
verifier.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +245,9 @@ protected void show(RevCommit c) throws Exception {
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
@ -252,6 +271,27 @@ protected void show(RevCommit c) throws Exception {
|
||||||
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,
|
||||||
|
|
|
@ -29,10 +29,15 @@
|
||||||
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;
|
||||||
|
@ -59,6 +64,9 @@ class Show extends TextBuiltin {
|
||||||
@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;
|
||||||
|
@ -220,13 +228,16 @@ private void show(RevTag tag) throws IOException {
|
||||||
}
|
}
|
||||||
|
|
||||||
outw.println();
|
outw.println();
|
||||||
String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$
|
String fullMessage = tag.getFullMessage();
|
||||||
for (String s : lines) {
|
if (!fullMessage.isEmpty()) {
|
||||||
outw.println(s);
|
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);
|
||||||
}
|
}
|
||||||
|
@ -258,6 +269,10 @@ private void show(RevWalk rw, RevCommit c) throws IOException {
|
||||||
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()));
|
||||||
|
@ -296,4 +311,28 @@ private void showDiff(RevCommit c) throws IOException {
|
||||||
}
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* 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
|
||||||
|
@ -22,43 +22,60 @@
|
||||||
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;
|
||||||
|
|
||||||
|
@ -70,7 +87,25 @@ class Tag extends TextBuiltin {
|
||||||
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()) {
|
||||||
|
@ -116,4 +151,36 @@ protected void run() {
|
||||||
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
|
@ -163,6 +163,7 @@ public static String fatalError(String message) {
|
||||||
/***/ 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;
|
||||||
|
|
|
@ -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.
|
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.
|
||||||
|
@ -763,6 +765,13 @@ uriNotFoundWithMessage={0} not found: {1}
|
||||||
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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
|
@ -772,6 +772,16 @@ public RemoteSetUrlCommand remoteSetUrl() {
|
||||||
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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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) 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
|
||||||
|
@ -650,6 +650,8 @@ public static JGitText get() {
|
||||||
/***/ 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;
|
||||||
|
@ -791,6 +793,13 @@ public static JGitText get() {
|
||||||
/***/ 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;
|
||||||
|
|
|
@ -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, 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
|
||||||
|
@ -343,6 +343,22 @@ public final String getTagName() {
|
||||||
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>
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue