]> source.dussan.org Git - jgit.git/commitdiff
Signing: refactor interfaces 21/1199821/3
authorThomas Wolf <twolf@apache.org>
Tue, 20 Aug 2024 20:41:45 +0000 (22:41 +0200)
committerThomas Wolf <twolf@apache.org>
Sat, 24 Aug 2024 19:53:21 +0000 (21:53 +0200)
This is a big API-breaking change cleaning up the signing interfaces.

Initially, these interfaces were GPG/OpenPGP-specific. When EGit added
new signers and signature verifiers that called an external GPG
executable, they were found inadequate and were extended to be able to
pass in the GpgConfig to get access to the "gpg.program" setting.

With the introduction of X.509 S/MIME signing, it was discovered that
the interfaces were still not quite adequate, and the "Gpg" prefix on
the class names were confusing.

Since 7.0 is a major version bump, I'm taking this chance to overhaul
these interfaces from ground up.

For signing, there is a new Signer interface. With it goes a
SignerFactory SPI interface, and a final Signers class managing the
currently set signers. By default, signers for the different signature
types are created from the signer factories, which are discovered via
the ServiceLoader. External code can install its own signers, overriding
the default factories.

For signature verification, exactly the same mechanism is used.

This simplifies the setup of signers and signature verifiers, and makes
it all more regular. Signer instances just get a byte[] to sign and
don't have to worry about ObjectBuilders at all. SignatureVerifier
instances also just get the data and signature as byte[] and don't have
to worry about extracting the signature from a commit or tag, or about
what kind of signature it is.

Both Signers and SignatureVerifiers always get passed the Repository
and the GpgConfig. The repository will be needed in an implementation
for SSH signatures because gpg.ssh.* configs may need to be loaded
explicitly, and some of those values need the current workspace
location.

For signature verification, there is exactly one place in core JGit in
SignatureVerifiers that extracts signatures, determines the signature
type, and then calls the right signature verifier.

Change RevTag to recognize all signature types known in git (GPG, X509,
and SSH).

Change-Id: I26d2731e7baebb38976c87b7f328b63a239760d5
Signed-off-by: Thomas Wolf <twolf@apache.org>
37 files changed:
org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
org.eclipse.jgit.gpg.bc/pom.xml
org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory [deleted file]
org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSigner [deleted file]
org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory [new file with mode: 0644]
org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory [new file with mode: 0644]
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java [deleted file]
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignerFactory.java [new file with mode: 0644]
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java
org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractGpgSignatureVerifier.java [deleted file]
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java [deleted file]
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java [deleted file]
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java [deleted file]
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java [deleted file]
org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java

index 9749ac111165593a4b869b7ce86c432cb26cbebe..7a62d1d2320649a21c38fcbf0b1be91259a734e8 100644 (file)
@@ -28,9 +28,6 @@ Import-Package: org.bouncycastle.asn1;version="[1.69.0,2.0.0)",
  org.bouncycastle.util;version="[1.69.0,2.0.0)",
  org.bouncycastle.util.encoders;version="[1.69.0,2.0.0)",
  org.bouncycastle.util.io;version="[1.69.0,2.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc;version="7.0.0",
- org.eclipse.jgit.gpg.bc.internal;version="7.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
+Export-Package: org.eclipse.jgit.gpg.bc.internal;version="7.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
  org.eclipse.jgit.gpg.bc.internal.keys;version="7.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test"
index f4dce68fabf6a7855712287ce3d54e6f2fd1852d..ad907d38095788f7ef1dd8180c17aba53bc4dcdf 100644 (file)
                   <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications>
                   <onlyBinaryIncompatible>false</onlyBinaryIncompatible>
                   <includeSynthetic>false</includeSynthetic>
-                  <ignoreMissingClasses>false</ignoreMissingClasses>
+                  <ignoreMissingClasses>true</ignoreMissingClasses>
                   <skipPomModules>true</skipPomModules>
               </parameter>
               <skip>false</skip>
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
deleted file mode 100644 (file)
index 17ab30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSigner b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSigner
deleted file mode 100644 (file)
index 6752b64..0000000
+++ /dev/null
@@ -1 +0,0 @@
-org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSigner
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory
new file mode 100644 (file)
index 0000000..17ab30f
--- /dev/null
@@ -0,0 +1 @@
+org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
new file mode 100644 (file)
index 0000000..c0b214d
--- /dev/null
@@ -0,0 +1 @@
+org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignerFactory
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java
deleted file mode 100644 (file)
index fdd1a2b..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.gpg.bc;
-
-import org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSigner;
-import org.eclipse.jgit.lib.GpgSigner;
-
-/**
- * Factory for creating a {@link GpgSigner} based on Bouncy Castle.
- *
- * @since 5.11
- */
-public final class BouncyCastleGpgSignerFactory {
-
-       private BouncyCastleGpgSignerFactory() {
-               // No instantiation
-       }
-
-       /**
-        * Creates a new {@link GpgSigner}.
-        *
-        * @return the {@link GpgSigner}
-        */
-       public static GpgSigner create() {
-               return new BouncyCastleGpgSigner();
-       }
-}
index 3378bb3969e779aeda4e97c0b2d20c0f6e2f52ef..5a3d43ba5434ea4a76d9ad3074fd81d050179d38 100644 (file)
@@ -12,7 +12,6 @@ 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.Date;
@@ -20,7 +19,6 @@ import java.util.List;
 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;
@@ -31,33 +29,20 @@ 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.api.errors.JGitInternalException;
-import org.eclipse.jgit.lib.AbstractGpgSignatureVerifier;
 import org.eclipse.jgit.lib.GpgConfig;
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SignatureVerifier;
 import org.eclipse.jgit.util.LRUMap;
 import org.eclipse.jgit.util.StringUtils;
 
 /**
- * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
+ * A {@link SignatureVerifier} to verify GPG signatures using BouncyCastle.
  */
 public class BouncyCastleGpgSignatureVerifier
-               extends AbstractGpgSignatureVerifier {
+               implements SignatureVerifier {
 
-       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();
-       }
+       private static final String NAME = "bc"; //$NON-NLS-1$
 
        // To support more efficient signature verification of multiple objects we
        // cache public keys once found in a LRU cache.
@@ -70,7 +55,7 @@ public class BouncyCastleGpgSignatureVerifier
 
        @Override
        public String getName() {
-               return "bc"; //$NON-NLS-1$
+               return NAME;
        }
 
        static PGPSignature parseSignature(InputStream in)
@@ -90,9 +75,8 @@ public class BouncyCastleGpgSignatureVerifier
        }
 
        @Override
-       public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
-                       byte[] signatureData)
-                       throws IOException {
+       public SignatureVerification verify(Repository repository, GpgConfig config,
+                       byte[] data, byte[] signatureData) throws IOException {
                PGPSignature signature = null;
                String fingerprint = null;
                String signer = null;
@@ -127,14 +111,15 @@ public class BouncyCastleGpgSignatureVerifier
                }
                Date signatureCreatedAt = signature.getCreationTime();
                if (fingerprint == null && signer == null && keyId == null) {
-                       return new VerificationResult(signatureCreatedAt, null, null, null,
-                                       false, false, TrustLevel.UNKNOWN,
+                       return new SignatureVerification(NAME, signatureCreatedAt,
+                                       null, null, null, false, false, TrustLevel.UNKNOWN,
                                        BCText.get().signatureNoKeyInfo);
                }
                if (fingerprint != null && keyId != null
                                && !fingerprint.endsWith(keyId)) {
-                       return new VerificationResult(signatureCreatedAt, signer, fingerprint,
-                                       signer, false, false, TrustLevel.UNKNOWN,
+                       return new SignatureVerification(NAME, signatureCreatedAt,
+                                       signer, fingerprint, signer, false, false,
+                                       TrustLevel.UNKNOWN,
                                        MessageFormat.format(BCText.get().signatureInconsistent,
                                                        keyId, fingerprint));
                }
@@ -175,15 +160,16 @@ public class BouncyCastleGpgSignatureVerifier
                                        bySigner.put(signer, NO_KEY);
                                }
                        }
-                       return new VerificationResult(signatureCreatedAt, signer,
-                                       fingerprint, signer, false, false, TrustLevel.UNKNOWN,
-                                       BCText.get().signatureNoPublicKey);
+                       return new SignatureVerification(NAME, signatureCreatedAt,
+                                       signer, fingerprint, signer, false, false,
+                                       TrustLevel.UNKNOWN, BCText.get().signatureNoPublicKey);
                }
                if (fingerprint != null && !publicKey.isExactMatch()) {
                        // We did find _some_ signing key for the signer, but it doesn't
                        // match the given fingerprint.
-                       return new VerificationResult(signatureCreatedAt, signer,
-                                       fingerprint, signer, false, false, TrustLevel.UNKNOWN,
+                       return new SignatureVerification(NAME, signatureCreatedAt,
+                                       signer, fingerprint, signer, false, false,
+                                       TrustLevel.UNKNOWN,
                                        MessageFormat.format(BCText.get().signatureNoSigningKey,
                                                        fingerprint));
                }
@@ -229,8 +215,7 @@ public class BouncyCastleGpgSignatureVerifier
                boolean verified = false;
                try {
                        signature.init(
-                                       new JcaPGPContentVerifierBuilderProvider()
-                                                       .setProvider(BouncyCastleProvider.PROVIDER_NAME),
+                                       new JcaPGPContentVerifierBuilderProvider(),
                                        pubKey);
                        signature.update(data);
                        verified = signature.verify();
@@ -238,15 +223,8 @@ public class BouncyCastleGpgSignatureVerifier
                        throw new JGitInternalException(
                                        BCText.get().signatureVerificationError, e);
                }
-               return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
-                               verified, expired, trust, null);
-       }
-
-       @Override
-       public SignatureVerification verify(byte[] data, byte[] signatureData)
-                       throws IOException {
-               throw new UnsupportedOperationException(
-                               "Call verify(GpgConfig, byte[], byte[]) instead."); //$NON-NLS-1$
+               return new SignatureVerification(NAME, signatureCreatedAt, signer,
+                               fingerprint, user, verified, expired, trust, null);
        }
 
        private TrustLevel parseGpgTrustPacket(byte[] packet) {
@@ -282,76 +260,4 @@ public class BouncyCastleGpgSignatureVerifier
                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;
-               }
-       }
 }
index ae82b758a6a35a80ad2c7e2c1b60a69ad17d8933..566ad1bf916b7385ad74ab9a90747a24d220e4c4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -9,20 +9,27 @@
  */
 package org.eclipse.jgit.gpg.bc.internal;
 
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
-import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.lib.SignatureVerifierFactory;
 
 /**
- * A {@link GpgSignatureVerifierFactory} that creates
- * {@link GpgSignatureVerifier} instances that verify GPG signatures using
- * BouncyCastle and that do cache public keys.
+ * A {@link SignatureVerifierFactory} that creates {@link SignatureVerifier}
+ * instances that verify GPG signatures using BouncyCastle and that do cache
+ * public keys.
  */
 public final class BouncyCastleGpgSignatureVerifierFactory
-               extends GpgSignatureVerifierFactory {
+               implements SignatureVerifierFactory {
 
        @Override
-       public GpgSignatureVerifier getVerifier() {
+       public GpgFormat getType() {
+               return GpgFormat.OPENPGP;
+       }
+
+       @Override
+       public SignatureVerifier create() {
                return new BouncyCastleGpgSignatureVerifier();
        }
 
+
 }
index 763b7f7526e4d302db0203ff0369798f8091cfeb..1d187a5db2aee1e71edb673c6566f00f7b0953c4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021, Salesforce and others
+ * Copyright (C) 2018, 2024, Salesforce and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -14,13 +14,11 @@ import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
-import java.security.Security;
 import java.util.Iterator;
 
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.bcpg.BCPGOutputStream;
 import org.bouncycastle.bcpg.HashAlgorithmTags;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPrivateKey;
 import org.bouncycastle.openpgp.PGPPublicKey;
@@ -30,79 +28,23 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator;
 import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
 import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
 import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
-import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.GpgConfig;
-import org.eclipse.jgit.lib.GpgObjectSigner;
 import org.eclipse.jgit.lib.GpgSignature;
-import org.eclipse.jgit.lib.GpgSigner;
-import org.eclipse.jgit.lib.ObjectBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.util.StringUtils;
 
 /**
  * GPG Signer using the BouncyCastle library.
  */
-public class BouncyCastleGpgSigner extends GpgSigner
-               implements GpgObjectSigner {
-
-       private static void registerBouncyCastleProviderIfNecessary() {
-               if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
-                       Security.addProvider(new BouncyCastleProvider());
-               }
-       }
-
-       /**
-        * Create a new instance.
-        * <p>
-        * The BounceCastleProvider will be registered if necessary.
-        * </p>
-        */
-       public BouncyCastleGpgSigner() {
-               registerBouncyCastleProviderIfNecessary();
-       }
-
-       @Override
-       public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
-                       PersonIdent committer, CredentialsProvider credentialsProvider)
-                       throws CanceledException {
-               try {
-                       return canLocateSigningKey(gpgSigningKey, committer,
-                                       credentialsProvider, null);
-               } catch (UnsupportedSigningFormatException e) {
-                       // Cannot occur with a null config
-                       return false;
-               }
-       }
-
-       @Override
-       public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
-                       PersonIdent committer, CredentialsProvider credentialsProvider,
-                       GpgConfig config)
-                       throws CanceledException, UnsupportedSigningFormatException {
-               if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
-                       throw new UnsupportedSigningFormatException(
-                                       JGitText.get().onlyOpenPgpSupportedForSigning);
-               }
-               try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
-                               credentialsProvider)) {
-                       BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
-                                       committer, passphrasePrompt);
-                       return gpgKey != null;
-               } catch (CanceledException e) {
-                       throw e;
-               } catch (Exception e) {
-                       return false;
-               }
-       }
+public class BouncyCastleGpgSigner implements Signer {
 
        private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
                        PersonIdent committer,
@@ -121,38 +63,24 @@ public class BouncyCastleGpgSigner extends GpgSigner
        }
 
        @Override
-       public void sign(@NonNull CommitBuilder commit,
-                       @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-                       CredentialsProvider credentialsProvider) throws CanceledException {
-               try {
-                       signObject(commit, gpgSigningKey, committer, credentialsProvider,
-                                       null);
-               } catch (UnsupportedSigningFormatException e) {
-                       // Cannot occur with a null config
-               }
-       }
-
-       @Override
-       public void signObject(@NonNull ObjectBuilder object,
-                       @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-                       CredentialsProvider credentialsProvider, GpgConfig config)
-                       throws CanceledException, UnsupportedSigningFormatException {
-               if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
-                       throw new UnsupportedSigningFormatException(
-                                       JGitText.get().onlyOpenPgpSupportedForSigning);
+       public GpgSignature sign(Repository repository, GpgConfig config,
+                       byte[] data, PersonIdent committer, String signingKey,
+                       CredentialsProvider credentialsProvider) throws CanceledException,
+                       IOException, UnsupportedSigningFormatException {
+               String gpgSigningKey = signingKey;
+               if (gpgSigningKey == null) {
+                       gpgSigningKey = config.getSigningKey();
                }
                try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
                                credentialsProvider)) {
                        BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
-                                       committer,
-                                               passphrasePrompt);
+                                       committer, passphrasePrompt);
                        PGPSecretKey secretKey = gpgKey.getSecretKey();
                        if (secretKey == null) {
                                throw new JGitInternalException(
                                                BCText.get().unableToSignCommitNoSecretKey);
                        }
-                       JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
-                                       .setProvider(BouncyCastleProvider.PROVIDER_NAME);
+                       JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder();
                        PGPPrivateKey privateKey = null;
                        if (!passphrasePrompt.hasPassphrase()) {
                                // Either the key is not encrypted, or it was read from the
@@ -177,8 +105,7 @@ public class BouncyCastleGpgSigner extends GpgSigner
                        PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
                                        new JcaPGPContentSignerBuilder(
                                                        publicKey.getAlgorithm(),
-                                                       HashAlgorithmTags.SHA256).setProvider(
-                                                                       BouncyCastleProvider.PROVIDER_NAME));
+                                                       HashAlgorithmTags.SHA256));
                        signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
                        PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
                        subpackets.setIssuerFingerprint(false, publicKey);
@@ -202,16 +129,36 @@ public class BouncyCastleGpgSigner extends GpgSigner
                        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                        try (BCPGOutputStream out = new BCPGOutputStream(
                                        new ArmoredOutputStream(buffer))) {
-                               signatureGenerator.update(object.build());
+                               signatureGenerator.update(data);
                                signatureGenerator.generate().encode(out);
                        }
-                       object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
-               } catch (PGPException | IOException | NoSuchAlgorithmException
+                       return new GpgSignature(buffer.toByteArray());
+               } catch (PGPException | NoSuchAlgorithmException
                                | NoSuchProviderException | URISyntaxException e) {
                        throw new JGitInternalException(e.getMessage(), e);
                }
        }
 
+       @Override
+       public boolean canLocateSigningKey(Repository repository, GpgConfig config,
+                       PersonIdent committer, String signingKey,
+                       CredentialsProvider credentialsProvider) throws CanceledException {
+               String gpgSigningKey = signingKey;
+               if (gpgSigningKey == null) {
+                       gpgSigningKey = config.getSigningKey();
+               }
+               try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
+                               credentialsProvider)) {
+                       BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
+                                       committer, passphrasePrompt);
+                       return gpgKey != null;
+               } catch (CanceledException e) {
+                       throw e;
+               } catch (Exception e) {
+                       return false;
+               }
+       }
+
        static String extractSignerId(String pgpUserId) {
                int from = pgpUserId.indexOf('<');
                if (from >= 0) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignerFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignerFactory.java
new file mode 100644 (file)
index 0000000..92ab65d
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.SignerFactory;
+
+/**
+ * Factory for creating a {@link Signer} for OPENPGP signatures based on Bouncy
+ * Castle.
+ */
+public final class BouncyCastleGpgSignerFactory implements SignerFactory {
+
+       @Override
+       public GpgFormat getType() {
+               return GpgFormat.OPENPGP;
+       }
+
+       @Override
+       public Signer create() {
+               return new BouncyCastleGpgSigner();
+       }
+}
index 852a4b377bbe7e5010bbe0d53c423f03babc6789..958e566986d6df0bf261e3134df7f381ee7a2c57 100644 (file)
@@ -32,13 +32,12 @@ import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.GpgConfig;
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
-import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
-import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SignatureVerifiers;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.internal.VerificationUtils;
@@ -174,8 +173,6 @@ class Log extends RevWalkTextBuiltin {
        // END -- Options shared with Diff
 
 
-       private GpgSignatureVerifier verifier;
-
        private GpgConfig config;
 
        Log() {
@@ -227,9 +224,6 @@ class Log extends RevWalkTextBuiltin {
                        throw die(e.getMessage(), e);
                } finally {
                        diffFmt.close();
-                       if (verifier != null) {
-                               verifier.clear();
-                       }
                }
        }
 
@@ -293,21 +287,13 @@ class Log extends RevWalkTextBuiltin {
                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);
+               SignatureVerification verification = SignatureVerifiers.verify(db,
+                               config, c);
                if (verification == null) {
                        return;
                }
                VerificationUtils.writeVerification(outw, verification,
-                               verifier.getName(), c.getCommitterIdent());
+                               verification.verifierName(), c.getCommitterIdent());
        }
 
        /**
index 4feb090032a9c43125412093177ebfb69063b3ce..1576792234fb8c603e66db65bb8e104d7ff891aa 100644 (file)
@@ -30,12 +30,11 @@ import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.GpgConfig;
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
-import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifiers;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.internal.VerificationUtils;
 import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
@@ -335,23 +334,13 @@ class Show extends TextBuiltin {
                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();
+               SignatureVerification verification = SignatureVerifiers.verify(db,
+                               config, c);
+               if (verification == null) {
+                       throw die(CLIText.get().logNoSignatureVerifier, null);
                }
+               VerificationUtils.writeVerification(outw, verification,
+                               verification.verifierName(), c.getCommitterIdent());
        }
 }
index 4ea67ab92c729961a7a241c8424fee0f2f9bef6f..6be30c944710cc76cd807cb8c61b72e48ed5d276 100644 (file)
@@ -27,10 +27,10 @@ import org.eclipse.jgit.api.VerifySignatureCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.internal.VerificationUtils;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -106,7 +106,8 @@ class Tag extends TextBuiltin {
                                                if (error != null) {
                                                        throw die(error.getMessage(), error);
                                                }
-                                               writeVerification(verifySig.getVerifier().getName(),
+                                               writeVerification(
+                                                               verification.getVerification().verifierName(),
                                                                (RevTag) verification.getObject(),
                                                                verification.getVerification());
                                        }
index c1f8a86a8cd636199162326c09216c91e92709e6..64ee6026200aae61aeabcc370646f0ee666dbc57 100644 (file)
@@ -11,7 +11,7 @@ package org.eclipse.jgit.pgm.internal;
 
 import java.io.IOException;
 
-import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.GitDateFormatter;
 import org.eclipse.jgit.util.SignatureUtils;
index 35de73e204228c188ef1a6f2d143a022d174badf..e74e234297cb14ca5743951159e81b71a5958523 100644 (file)
@@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.EmptyCommitException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.dircache.DirCache;
@@ -34,19 +35,23 @@ import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.junit.time.TimeUtil;
-import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.GpgSigner;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.ObjectBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
 import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.submodule.SubmoduleWalk;
@@ -839,21 +844,39 @@ public class CommitCommandTest extends RepositoryTestCase {
                        String[] signingKey = new String[1];
                        PersonIdent[] signingCommitters = new PersonIdent[1];
                        AtomicInteger callCount = new AtomicInteger();
-                       GpgSigner.setDefault(new GpgSigner() {
+                       // Since GpgFormat defaults to OpenPGP just set a new signer for
+                       // that.
+                       Signers.set(GpgFormat.OPENPGP, new Signer() {
+
                                @Override
-                               public void sign(CommitBuilder commit, String gpgSigningKey,
-                                               PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
-                                       signingKey[0] = gpgSigningKey;
+                               public void signObject(Repository repo, GpgConfig config,
+                                               ObjectBuilder builder, PersonIdent signingCommitter,
+                                               String signingKeySpec,
+                                               CredentialsProvider credentialsProvider)
+                                               throws CanceledException,
+                                               UnsupportedSigningFormatException {
+                                       signingKey[0] = signingKeySpec;
                                        signingCommitters[0] = signingCommitter;
                                        callCount.incrementAndGet();
                                }
 
                                @Override
-                               public boolean canLocateSigningKey(String gpgSigningKey,
-                                               PersonIdent signingCommitter,
+                               public GpgSignature sign(Repository repo, GpgConfig config,
+                                               byte[] data, PersonIdent signingCommitter,
+                                               String signingKeySpec,
+                                               CredentialsProvider credentialsProvider)
+                                               throws CanceledException,
+                                               UnsupportedSigningFormatException {
+                                       throw new CanceledException("Unexpected call");
+                               }
+
+                               @Override
+                               public boolean canLocateSigningKey(Repository repo,
+                                               GpgConfig config, PersonIdent signingCommitter,
+                                               String signingKeySpec,
                                                CredentialsProvider credentialsProvider)
                                                throws CanceledException {
-                                       return false;
+                                       throw new CanceledException("Unexpected call");
                                }
                        });
 
@@ -904,19 +927,37 @@ public class CommitCommandTest extends RepositoryTestCase {
                        git.add().addFilepattern("file1").call();
 
                        AtomicInteger callCount = new AtomicInteger();
-                       GpgSigner.setDefault(new GpgSigner() {
+                       // Since GpgFormat defaults to OpenPGP just set a new signer for
+                       // that.
+                       Signers.set(GpgFormat.OPENPGP, new Signer() {
+
                                @Override
-                               public void sign(CommitBuilder commit, String gpgSigningKey,
-                                               PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
+                               public void signObject(Repository repo, GpgConfig config,
+                                               ObjectBuilder builder, PersonIdent signingCommitter,
+                                               String signingKeySpec,
+                                               CredentialsProvider credentialsProvider)
+                                               throws CanceledException,
+                                               UnsupportedSigningFormatException {
                                        callCount.incrementAndGet();
                                }
 
                                @Override
-                               public boolean canLocateSigningKey(String gpgSigningKey,
-                                               PersonIdent signingCommitter,
+                               public GpgSignature sign(Repository repo, GpgConfig config,
+                                               byte[] data, PersonIdent signingCommitter,
+                                               String signingKeySpec,
+                                               CredentialsProvider credentialsProvider)
+                                               throws CanceledException,
+                                               UnsupportedSigningFormatException {
+                                       throw new CanceledException("Unexpected call");
+                               }
+
+                               @Override
+                               public boolean canLocateSigningKey(Repository repo,
+                                               GpgConfig config, PersonIdent signingCommitter,
+                                               String signingKeySpec,
                                                CredentialsProvider credentialsProvider)
                                                throws CanceledException {
-                                       return false;
+                                       throw new CanceledException("Unexpected call");
                                }
                        });
 
index c9f7336609688d7c2aa9cef78e1f96227e24726e..11435b8ce45c652695f85d6af23e6b6aaf78f905 100644 (file)
@@ -576,7 +576,6 @@ obtainingCommitsForCherryPick=Obtaining commits that need to be cherry-picked
 oldIdMustNotBeNull=Expected old ID must not be null
 onlyOneFetchSupported=Only one fetch supported
 onlyOneOperationCallPerConnectionIsSupported=Only one operation call per connection is supported.
-onlyOpenPgpSupportedForSigning=OpenPGP is the only supported signing option with JGit at this time (gpg.format must be set to openpgp).
 openFilesMustBeAtLeast1=Open files must be >= 1
 openingConnection=Opening connection
 operationCanceled=Operation {0} was canceled
@@ -723,6 +722,8 @@ shortSkipOfBlock=Short skip of block.
 shutdownCleanup=Cleanup {} during JVM shutdown
 shutdownCleanupFailed=Cleanup during JVM shutdown failed
 shutdownCleanupListenerFailed=Cleanup of {0} during JVM shutdown failed
+signatureServiceConflict={0} conflict for type {1}. Already registered is {2}; additional factory {3} is ignored.
+signatureTypeUnknown=No signer for {0} signatures. Use another signature type for git config gpg.format, or do not sign.
 signatureVerificationError=Signature verification failed
 signatureVerificationUnavailable=No signature verifier registered
 signedTagMessageNoLf=A non-empty message of a signed tag must end in LF.
index a1a2cc09d22b4002db6fa29346264c8caed97622..a7d409c3f58be75a24f83b0cf19a5ddba84be500 100644 (file)
@@ -51,9 +51,6 @@ import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.GpgConfig;
-import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
-import org.eclipse.jgit.lib.GpgObjectSigner;
-import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -62,6 +59,8 @@ import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -129,7 +128,7 @@ public class CommitCommand extends GitCommand<RevCommit> {
 
        private String signingKey;
 
-       private GpgSigner gpgSigner;
+       private Signer signer;
 
        private GpgConfig gpgConfig;
 
@@ -319,30 +318,22 @@ public class CommitCommand extends GitCommand<RevCommit> {
                }
        }
 
-       private void sign(CommitBuilder commit) throws ServiceUnavailableException,
-                       CanceledException, UnsupportedSigningFormatException {
-               if (gpgSigner == null) {
-                       gpgSigner = GpgSigner.getDefault();
-                       if (gpgSigner == null) {
-                               throw new ServiceUnavailableException(
-                                               JGitText.get().signingServiceUnavailable);
+       private void sign(CommitBuilder commit)
+                       throws CanceledException, IOException,
+                       UnsupportedSigningFormatException {
+               if (signer == null) {
+                       signer = Signers.get(gpgConfig.getKeyFormat());
+                       if (signer == null) {
+                               throw new UnsupportedSigningFormatException(MessageFormat
+                                               .format(JGitText.get().signatureTypeUnknown,
+                                                               gpgConfig.getKeyFormat().toConfigValue()));
                        }
                }
                if (signingKey == null) {
                        signingKey = gpgConfig.getSigningKey();
                }
-               if (gpgSigner instanceof GpgObjectSigner) {
-                       ((GpgObjectSigner) gpgSigner).signObject(commit,
-                                       signingKey, committer, credentialsProvider,
-                                       gpgConfig);
-               } else {
-                       if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
-                               throw new UnsupportedSigningFormatException(JGitText
-                                               .get().onlyOpenPgpSupportedForSigning);
-                       }
-                       gpgSigner.sign(commit, signingKey, committer,
-                                       credentialsProvider);
-               }
+               signer.signObject(repo, gpgConfig, commit, committer, signingKey,
+                               credentialsProvider);
        }
 
        private void updateRef(RepositoryState state, ObjectId headId,
@@ -1097,22 +1088,22 @@ public class CommitCommand extends GitCommand<RevCommit> {
        }
 
        /**
-        * Sets the {@link GpgSigner} to use if the commit is to be signed.
+        * Sets the {@link Signer} to use if the commit is to be signed.
         *
         * @param signer
         *            to use; if {@code null}, the default signer will be used
         * @return {@code this}
-        * @since 5.11
+        * @since 7.0
         */
-       public CommitCommand setGpgSigner(GpgSigner signer) {
+       public CommitCommand setSigner(Signer signer) {
                checkCallable();
-               this.gpgSigner = signer;
+               this.signer = signer;
                return this;
        }
 
        /**
         * Sets an external {@link GpgConfig} to use. Whether it will be used is at
-        * the discretion of the {@link #setGpgSigner(GpgSigner)}.
+        * the discretion of the {@link #setSigner(Signer)}.
         *
         * @param config
         *            to set; if {@code null}, the config will be loaded from the
index 3edaf5e748b9739b49510c7e6ec10800ee64261f..cc8589fa1c5acf68ab12239a6419e199ad16c799 100644 (file)
@@ -18,14 +18,11 @@ import org.eclipse.jgit.api.errors.InvalidTagNameException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
-import org.eclipse.jgit.api.errors.ServiceUnavailableException;
 import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.GpgConfig;
 import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
-import org.eclipse.jgit.lib.GpgObjectSigner;
-import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -33,6 +30,8 @@ import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
 import org.eclipse.jgit.lib.TagBuilder;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -79,7 +78,7 @@ public class TagCommand extends GitCommand<Ref> {
 
        private GpgConfig gpgConfig;
 
-       private GpgObjectSigner gpgSigner;
+       private Signer signer;
 
        private CredentialsProvider credentialsProvider;
 
@@ -133,9 +132,9 @@ public class TagCommand extends GitCommand<Ref> {
                        newTag.setTagger(tagger);
                        newTag.setObjectId(id);
 
-                       if (gpgSigner != null) {
-                               gpgSigner.signObject(newTag, signingKey, tagger,
-                                               credentialsProvider, gpgConfig);
+                       if (signer != null) {
+                               signer.signObject(repo, gpgConfig, newTag, tagger, signingKey,
+                                               credentialsProvider);
                        }
 
                        // write the tag object
@@ -196,15 +195,12 @@ public class TagCommand extends GitCommand<Ref> {
         *
         * @throws InvalidTagNameException
         *             if the tag name is null or invalid
-        * @throws ServiceUnavailableException
-        *             if the tag should be signed but no signer can be found
         * @throws UnsupportedSigningFormatException
         *             if the tag should be signed but {@code gpg.format} is not
         *             {@link GpgFormat#OPENPGP}
         */
        private void processOptions()
-                       throws InvalidTagNameException, ServiceUnavailableException,
-                       UnsupportedSigningFormatException {
+                       throws InvalidTagNameException, UnsupportedSigningFormatException {
                if (name == null
                                || !Repository.isValidRefName(Constants.R_TAGS + name)) {
                        throw new InvalidTagNameException(
@@ -230,16 +226,15 @@ public class TagCommand extends GitCommand<Ref> {
                                        doSign = gpgConfig.isSignAnnotated();
                                }
                                if (doSign) {
-                                       if (signingKey == null) {
-                                               signingKey = gpgConfig.getSigningKey();
-                                       }
-                                       if (gpgSigner == null) {
-                                               GpgSigner signer = GpgSigner.getDefault();
-                                               if (!(signer instanceof GpgObjectSigner)) {
-                                                       throw new ServiceUnavailableException(
-                                                                       JGitText.get().signingServiceUnavailable);
+                                       if (signer == null) {
+                                               signer = Signers.get(gpgConfig.getKeyFormat());
+                                               if (signer == null) {
+                                                       throw new UnsupportedSigningFormatException(
+                                                                       MessageFormat.format(
+                                                                                       JGitText.get().signatureTypeUnknown,
+                                                                                       gpgConfig.getKeyFormat()
+                                                                                                       .toConfigValue()));
                                                }
-                                               gpgSigner = (GpgObjectSigner) signer;
                                        }
                                        // The message of a signed tag must end in a newline because
                                        // the signature will be appended.
@@ -326,22 +321,22 @@ public class TagCommand extends GitCommand<Ref> {
        }
 
        /**
-        * Sets the {@link GpgSigner} to use if the commit is to be signed.
+        * Sets the {@link Signer} to use if the commit is to be signed.
         *
         * @param signer
         *            to use; if {@code null}, the default signer will be used
         * @return {@code this}
-        * @since 5.11
+        * @since 7.0
         */
-       public TagCommand setGpgSigner(GpgObjectSigner signer) {
+       public TagCommand setSigner(Signer signer) {
                checkCallable();
-               this.gpgSigner = signer;
+               this.signer = signer;
                return this;
        }
 
        /**
         * Sets an external {@link GpgConfig} to use. Whether it will be used is at
-        * the discretion of the {@link #setGpgSigner(GpgObjectSigner)}.
+        * the discretion of the {@link #setSigner(Signer)}.
         *
         * @param config
         *            to set; if {@code null}, the config will be loaded from the
index 21cddf75b723d53cd3776da7e2debe2c57dc9c98..f5f4b06e4598ee9c17f1e90810ea6d0c9670a15f 100644 (file)
@@ -9,7 +9,7 @@
  */
 package org.eclipse.jgit.api;
 
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.SignatureVerifier;
 import org.eclipse.jgit.revwalk.RevObject;
 
 /**
@@ -34,8 +34,9 @@ public interface VerificationResult {
         * Retrieves the signature verification result.
         *
         * @return the result, or {@code null} if none was computed
+        * @since 7.0
         */
-       GpgSignatureVerifier.SignatureVerification getVerification();
+       SignatureVerifier.SignatureVerification getVerification();
 
        /**
         * Retrieves the git object of which the signature was verified.
index 6a2a44ea2db87ecf154a9b4145cd376cda378a51..487ff043237ebadbfb54bf8232534970a6f509d0 100644 (file)
@@ -25,11 +25,10 @@ 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.lib.SignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifiers;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 
@@ -65,12 +64,8 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
 
        private VerifyMode mode = VerifyMode.ANY;
 
-       private GpgSignatureVerifier verifier;
-
        private GpgConfig config;
 
-       private boolean ownVerifier;
-
        /**
         * Creates a new {@link VerifySignatureCommand} for the given {@link Repository}.
         *
@@ -140,22 +135,7 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
        }
 
        /**
-        * 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)}.
+        * Sets an external {@link GpgConfig} to use.
         *
         * @param config
         *            to set; if {@code null}, the config will be loaded from the
@@ -169,16 +149,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
                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
@@ -193,9 +163,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
         *
         * @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
@@ -207,16 +174,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
                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());
                }
@@ -239,10 +196,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
                } catch (IOException e) {
                        throw new JGitInternalException(
                                        JGitText.get().signatureVerificationError, e);
-               } finally {
-                       if (ownVerifier) {
-                               verifier.clear();
-                       }
                }
                return result;
        }
@@ -258,8 +211,8 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR
                }
                if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) {
                        try {
-                               GpgSignatureVerifier.SignatureVerification verification = verifier
-                                               .verifySignature(object, config);
+                               SignatureVerification verification = SignatureVerifiers
+                                               .verify(repo, config, object);
                                if (verification == null) {
                                        // Not signed
                                        return null;
index 8a5f2b2b3096e21fef8029ff75afce14b27dbb5b..310962b9678cd0be2bdf1877e9477e11e5680c13 100644 (file)
@@ -606,7 +606,6 @@ public class JGitText extends TranslationBundle {
        /***/ public String oldIdMustNotBeNull;
        /***/ public String onlyOneFetchSupported;
        /***/ public String onlyOneOperationCallPerConnectionIsSupported;
-       /***/ public String onlyOpenPgpSupportedForSigning;
        /***/ public String openFilesMustBeAtLeast1;
        /***/ public String openingConnection;
        /***/ public String operationCanceled;
@@ -752,6 +751,8 @@ public class JGitText extends TranslationBundle {
        /***/ public String shutdownCleanup;
        /***/ public String shutdownCleanupFailed;
        /***/ public String shutdownCleanupListenerFailed;
+       /***/ public String signatureServiceConflict;
+       /***/ public String signatureTypeUnknown;
        /***/ public String signatureVerificationError;
        /***/ public String signatureVerificationUnavailable;
        /***/ public String signedTagMessageNoLf;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractGpgSignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractGpgSignatureVerifier.java
deleted file mode 100644 (file)
index 06a89dc..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others
-*
-* This program and the accompanying materials are made available under the
-* terms of the Eclipse Distribution License v. 1.0 which is available at
-* 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.Arrays;
-
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * Provides a base implementation of
- * {@link GpgSignatureVerifier#verifySignature(RevObject, GpgConfig)}.
- *
- * @since 6.9
- */
-public abstract class AbstractGpgSignatureVerifier
-               implements GpgSignatureVerifier {
-
-       @Override
-       public SignatureVerification verifySignature(RevObject object,
-                       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.nextLfSkippingSplitLines(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(config, 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(config, data, signatureData);
-               }
-               return null;
-       }
-}
index b9c90bda30ef0cf3850f3b05ec849c4f9c150457..8599bf7dbc9c73e8821a28eeed45723f6609be57 100644 (file)
@@ -552,6 +552,20 @@ public final class Constants {
         */
        public static final String GPG_SIGNATURE_PREFIX = "-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$
 
+       /**
+        * Prefix of a CMS signature (X.509, S/MIME).
+        *
+        * @since 7.0
+        */
+       public static final String CMS_SIGNATURE_PREFIX = "-----BEGIN SIGNED MESSAGE-----"; //$NON-NLS-1$
+
+       /**
+        * Prefix of an SSH signature.
+        *
+        * @since 7.0
+        */
+       public static final String SSH_SIGNATURE_PREFIX = "-----BEGIN SSH SIGNATURE-----"; //$NON-NLS-1$
+
        /**
         * Create a new digest function for objects.
         *
index f5064df0614ae8ddc567afca963c3406940e93cc..fb5c904215215274b14d2ae3c287b743775ecf88 100644 (file)
@@ -61,27 +61,6 @@ public class GpgConfig {
 
        private final boolean forceAnnotated;
 
-       /**
-        * Create a {@link GpgConfig} with the given parameters and default
-        * {@code true} for signing commits and {@code false} for tags.
-        *
-        * @param keySpec
-        *            to use
-        * @param format
-        *            to use
-        * @param gpgProgram
-        *            to use
-        * @since 5.11
-        */
-       public GpgConfig(String keySpec, GpgFormat format, String gpgProgram) {
-               keyFormat = format;
-               signingKey = keySpec;
-               program = gpgProgram;
-               signCommits = true;
-               signAllTags = false;
-               forceAnnotated = false;
-       }
-
        /**
         * Create a new GPG config that reads the configuration from config.
         *
@@ -97,10 +76,11 @@ public class GpgConfig {
 
                String exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION,
                                keyFormat.toConfigValue(), ConfigConstants.CONFIG_KEY_PROGRAM);
-               if (exe == null) {
+               if (exe == null && GpgFormat.OPENPGP.equals(keyFormat)) {
                        exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION, null,
                                        ConfigConstants.CONFIG_KEY_PROGRAM);
                }
+
                program = exe;
                signCommits = config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION,
                                ConfigConstants.CONFIG_KEY_GPGSIGN, false);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
deleted file mode 100644 (file)
index 074f465..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2020 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 org.eclipse.jgit.annotations.NonNull;
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.api.errors.CanceledException;
-import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
-import org.eclipse.jgit.transport.CredentialsProvider;
-
-/**
- * Creates GPG signatures for Git objects.
- *
- * @since 5.11
- */
-public interface GpgObjectSigner {
-
-       /**
-        * Signs the specified object.
-        *
-        * <p>
-        * Implementors should obtain the payload for signing from the specified
-        * object via {@link ObjectBuilder#build()} and create a proper
-        * {@link GpgSignature}. The generated signature must be set on the
-        * specified {@code object} (see
-        * {@link ObjectBuilder#setGpgSignature(GpgSignature)}).
-        * </p>
-        * <p>
-        * Any existing signature on the object must be discarded prior obtaining
-        * the payload via {@link ObjectBuilder#build()}.
-        * </p>
-        *
-        * @param object
-        *            the object to sign (must not be {@code null} and must be
-        *            complete to allow proper calculation of payload)
-        * @param gpgSigningKey
-        *            the signing key to locate (passed as is to the GPG signing
-        *            tool as is; eg., value of <code>user.signingkey</code>)
-        * @param committer
-        *            the signing identity (to help with key lookup in case signing
-        *            key is not specified)
-        * @param credentialsProvider
-        *            provider to use when querying for signing key credentials (eg.
-        *            passphrase)
-        * @param config
-        *            GPG settings from the git config
-        * @throws CanceledException
-        *             when signing was canceled (eg., user aborted when entering
-        *             passphrase)
-        * @throws UnsupportedSigningFormatException
-        *             if a config is given and the wanted key format is not
-        *             supported
-        */
-       void signObject(@NonNull ObjectBuilder object,
-                       @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-                       CredentialsProvider credentialsProvider, GpgConfig config)
-                       throws CanceledException, UnsupportedSigningFormatException;
-
-       /**
-        * Indicates if a signing key is available for the specified committer
-        * and/or signing key.
-        *
-        * @param gpgSigningKey
-        *            the signing key to locate (passed as is to the GPG signing
-        *            tool as is; eg., value of <code>user.signingkey</code>)
-        * @param committer
-        *            the signing identity (to help with key lookup in case signing
-        *            key is not specified)
-        * @param credentialsProvider
-        *            provider to use when querying for signing key credentials (eg.
-        *            passphrase)
-        * @param config
-        *            GPG settings from the git config
-        * @return <code>true</code> if a signing key is available,
-        *         <code>false</code> otherwise
-        * @throws CanceledException
-        *             when signing was canceled (eg., user aborted when entering
-        *             passphrase)
-        * @throws UnsupportedSigningFormatException
-        *             if a config is given and the wanted key format is not
-        *             supported
-        */
-       public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey,
-                       @NonNull PersonIdent committer,
-                       CredentialsProvider credentialsProvider, GpgConfig config)
-                       throws CanceledException, UnsupportedSigningFormatException;
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java
deleted file mode 100644 (file)
index 91c9bab..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * 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 GpgSignatureVerifier} 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 config
-        *            the {@link GpgConfig}
-        * @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
-        * @since 6.9
-        */
-       default SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
-                       byte[] signatureData) throws IOException {
-               // Default implementation for backwards compatibility; override as
-               // appropriate
-               return verify(data, signatureData);
-       }
-
-       /**
-        * 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
-        * @deprecated since 6.9, use {@link #verify(GpgConfig, byte[], byte[])}
-        *             instead
-        */
-       @Deprecated
-       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
-       }
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
deleted file mode 100644 (file)
index 59775c4..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2021, 2022 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 class DefaultFactory {
-
-               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;
-               }
-
-               private DefaultFactory() {
-                       // No instantiation
-               }
-
-               public static GpgSignatureVerifierFactory getDefault() {
-                       return defaultFactory;
-               }
-
-               /**
-                * Sets the default factory.
-                *
-                * @param factory
-                *            the new default factory
-                */
-               public static void setDefault(GpgSignatureVerifierFactory factory) {
-                       defaultFactory = factory;
-               }
-       }
-
-       /**
-        * Retrieves the default factory.
-        *
-        * @return the default factory or {@code null} if none set
-        */
-       public static GpgSignatureVerifierFactory getDefault() {
-               return DefaultFactory.getDefault();
-       }
-
-       /**
-        * Sets the default factory.
-        *
-        * @param factory
-        *            the new default factory
-        */
-       public static void setDefault(GpgSignatureVerifierFactory factory) {
-               DefaultFactory.setDefault(factory);
-       }
-
-       /**
-        * Creates a new {@link GpgSignatureVerifier}.
-        *
-        * @return the new {@link GpgSignatureVerifier}
-        */
-       public abstract GpgSignatureVerifier getVerifier();
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
deleted file mode 100644 (file)
index b25a61b..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2018, 2022 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.lib;
-
-import java.util.Iterator;
-import java.util.ServiceConfigurationError;
-import java.util.ServiceLoader;
-
-import org.eclipse.jgit.annotations.NonNull;
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.api.errors.CanceledException;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Creates GPG signatures for Git objects.
- *
- * @since 5.3
- */
-public abstract class GpgSigner {
-
-       private static final Logger LOG = LoggerFactory.getLogger(GpgSigner.class);
-
-       private static class DefaultSigner {
-
-               private static volatile GpgSigner defaultSigner = loadGpgSigner();
-
-               private static GpgSigner loadGpgSigner() {
-                       try {
-                               ServiceLoader<GpgSigner> loader = ServiceLoader
-                                               .load(GpgSigner.class);
-                               Iterator<GpgSigner> iter = loader.iterator();
-                               if (iter.hasNext()) {
-                                       return iter.next();
-                               }
-                       } catch (ServiceConfigurationError e) {
-                               LOG.error(e.getMessage(), e);
-                       }
-                       return null;
-               }
-
-               private DefaultSigner() {
-                       // No instantiation
-               }
-
-               public static GpgSigner getDefault() {
-                       return defaultSigner;
-               }
-
-               public static void setDefault(GpgSigner signer) {
-                       defaultSigner = signer;
-               }
-       }
-
-       /**
-        * Get the default signer, or <code>null</code>.
-        *
-        * @return the default signer, or <code>null</code>.
-        */
-       public static GpgSigner getDefault() {
-               return DefaultSigner.getDefault();
-       }
-
-       /**
-        * Set the default signer.
-        *
-        * @param signer
-        *            the new default signer, may be <code>null</code> to select no
-        *            default.
-        */
-       public static void setDefault(GpgSigner signer) {
-               DefaultSigner.setDefault(signer);
-       }
-
-       /**
-        * Signs the specified commit.
-        *
-        * <p>
-        * Implementors should obtain the payload for signing from the specified
-        * commit via {@link CommitBuilder#build()} and create a proper
-        * {@link GpgSignature}. The generated signature must be set on the
-        * specified {@code commit} (see
-        * {@link CommitBuilder#setGpgSignature(GpgSignature)}).
-        * </p>
-        * <p>
-        * Any existing signature on the commit must be discarded prior obtaining
-        * the payload via {@link CommitBuilder#build()}.
-        * </p>
-        *
-        * @param commit
-        *            the commit to sign (must not be <code>null</code> and must be
-        *            complete to allow proper calculation of payload)
-        * @param gpgSigningKey
-        *            the signing key to locate (passed as is to the GPG signing
-        *            tool as is; eg., value of <code>user.signingkey</code>)
-        * @param committer
-        *            the signing identity (to help with key lookup in case signing
-        *            key is not specified)
-        * @param credentialsProvider
-        *            provider to use when querying for signing key credentials (eg.
-        *            passphrase)
-        * @throws CanceledException
-        *             when signing was canceled (eg., user aborted when entering
-        *             passphrase)
-        */
-       public abstract void sign(@NonNull CommitBuilder commit,
-                       @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-                       CredentialsProvider credentialsProvider) throws CanceledException;
-
-       /**
-        * Indicates if a signing key is available for the specified committer
-        * and/or signing key.
-        *
-        * @param gpgSigningKey
-        *            the signing key to locate (passed as is to the GPG signing
-        *            tool as is; eg., value of <code>user.signingkey</code>)
-        * @param committer
-        *            the signing identity (to help with key lookup in case signing
-        *            key is not specified)
-        * @param credentialsProvider
-        *            provider to use when querying for signing key credentials (eg.
-        *            passphrase)
-        * @return <code>true</code> if a signing key is available,
-        *         <code>false</code> otherwise
-        * @throws CanceledException
-        *             when signing was canceled (eg., user aborted when entering
-        *             passphrase)
-        */
-       public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey,
-                       @NonNull PersonIdent committer,
-                       CredentialsProvider credentialsProvider) throws CanceledException;
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java
new file mode 100644 (file)
index 0000000..2ce2708
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * 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.api.errors.JGitInternalException;
+
+/**
+ * A {@code SignatureVerifier} can verify signatures on git commits and tags.
+ *
+ * @since 7.0
+ */
+public interface SignatureVerifier {
+
+       /**
+        * Verifies a given signature for given data.
+        *
+        * @param repository
+        *            the {@link Repository} the data comes from.
+        * @param config
+        *            the {@link GpgConfig}
+        * @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
+        */
+       SignatureVerification verify(@NonNull Repository repository,
+                       @NonNull GpgConfig config, 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 SignatureVerifier} 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.
+        *
+        * @param verifierName
+        *            the name of the verifier that created this verification result
+        * @param creationDate
+        *            date and time the signature was created
+        * @param signer
+        *            the signer as stored in the signature, or {@code null} if
+        *            unknown
+        * @param keyFingerprint
+        *            fingerprint of the public key, or {@code null} if unknown
+        * @param keyUser
+        *            user associated with the key, or {@code null} if unknown
+        * @param verified
+        *            whether the signature verification was successful
+        * @param expired
+        *            whether the public key used for this signature verification
+        *            was expired when the signature was created
+        * @param trustLevel
+        *            the trust level of the public key used to verify the signature
+        * @param message
+        *            human-readable message giving additional information about the
+        *            outcome of the verification, possibly {@code null}
+        */
+       record SignatureVerification(
+                       String verifierName,
+                       Date creationDate,
+                       String signer,
+                       String keyFingerprint,
+                       String keyUser,
+                       boolean verified,
+                       boolean expired,
+                       @NonNull TrustLevel trustLevel,
+                       String message) {
+       }
+
+       /**
+        * The owner's trust in a public key.
+        */
+       enum TrustLevel {
+               UNKNOWN, NEVER, MARGINAL, FULL, ULTIMATE
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java
new file mode 100644 (file)
index 0000000..7844aba
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A factory for {@link SignatureVerifier}s.
+ *
+ * @since 7.0
+ */
+public interface SignatureVerifierFactory {
+
+       /**
+        * Tells what kind of {@link SignatureVerifier} this factory creates.
+        *
+        * @return the {@link GpgConfig.GpgFormat} of the signer
+        */
+       @NonNull
+       GpgConfig.GpgFormat getType();
+
+       /**
+        * Creates a new instance of a {@link SignatureVerifier} that can produce
+        * signatures of type {@link #getType()}.
+        *
+        * @return a new {@link SignatureVerifier}
+        */
+       @NonNull
+       SignatureVerifier create();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java
new file mode 100644 (file)
index 0000000..01c8422
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * 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.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the available signers.
+ *
+ * @since 7.0
+ */
+public final class SignatureVerifiers {
+
+       private static final Logger LOG = LoggerFactory.getLogger(SignatureVerifiers.class);
+
+       private static final byte[] PGP_PREFIX = Constants.GPG_SIGNATURE_PREFIX
+                       .getBytes(StandardCharsets.US_ASCII);
+
+       private static final byte[] X509_PREFIX = Constants.CMS_SIGNATURE_PREFIX
+                       .getBytes(StandardCharsets.US_ASCII);
+
+       private static final byte[] SSH_PREFIX = Constants.SSH_SIGNATURE_PREFIX
+                       .getBytes(StandardCharsets.US_ASCII);
+
+       private static final Map<GpgConfig.GpgFormat, SignatureVerifierFactory> FACTORIES = loadSignatureVerifiers();
+
+       private static final Map<GpgConfig.GpgFormat, SignatureVerifier> VERIFIERS = new ConcurrentHashMap<>();
+
+       private static Map<GpgConfig.GpgFormat, SignatureVerifierFactory> loadSignatureVerifiers() {
+               Map<GpgConfig.GpgFormat, SignatureVerifierFactory> result = new EnumMap<>(
+                               GpgConfig.GpgFormat.class);
+               try {
+                       for (SignatureVerifierFactory factory : ServiceLoader
+                                       .load(SignatureVerifierFactory.class)) {
+                               GpgConfig.GpgFormat format = factory.getType();
+                               SignatureVerifierFactory existing = result.get(format);
+                               if (existing != null) {
+                                       LOG.warn("{}", //$NON-NLS-1$
+                                                       MessageFormat.format(
+                                                                       JGitText.get().signatureServiceConflict,
+                                                                       "SignatureVerifierFactory", format, //$NON-NLS-1$
+                                                                       existing.getClass().getCanonicalName(),
+                                                                       factory.getClass().getCanonicalName()));
+                               } else {
+                                       result.put(format, factory);
+                               }
+                       }
+               } catch (ServiceConfigurationError e) {
+                       LOG.error(e.getMessage(), e);
+               }
+               return result;
+       }
+
+       private SignatureVerifiers() {
+               // No instantiation
+       }
+
+       /**
+        * Retrieves a {@link Signer} that can produce signatures of the given type
+        * {@code format}.
+        *
+        * @param format
+        *            {@link GpgConfig.GpgFormat} the signer must support
+        * @return a {@link Signer}, or {@code null} if none is available
+        */
+       public static SignatureVerifier get(@NonNull GpgConfig.GpgFormat format) {
+               return VERIFIERS.computeIfAbsent(format, f -> {
+                       SignatureVerifierFactory factory = FACTORIES.get(format);
+                       if (factory == null) {
+                               return null;
+                       }
+                       return factory.create();
+               });
+       }
+
+       /**
+        * Sets a specific signature verifier to use for a specific signature type.
+        *
+        * @param format
+        *            signature type to set the {@code verifier} for
+        * @param verifier
+        *            the {@link SignatureVerifier} to use for signatures of type
+        *            {@code format}; if {@code null}, a default implementation, if
+        *            available, may be used.
+        */
+       public static void set(@NonNull GpgConfig.GpgFormat format,
+                       SignatureVerifier verifier) {
+               SignatureVerifier previous;
+               if (verifier == null) {
+                       previous = VERIFIERS.remove(format);
+               } else {
+                       previous = VERIFIERS.put(format, verifier);
+               }
+               if (previous != null) {
+                       previous.clear();
+               }
+       }
+
+       /**
+        * Verifies the signature on a signed commit or tag.
+        *
+        * @param repository
+        *            the {@link Repository} the object is from
+        * @param config
+        *            the {@link GpgConfig} to use
+        * @param object
+        *            to verify
+        * @return a {@link SignatureVerifier.SignatureVerification} describing the
+        *         outcome of the verification, or {@code null} if the object does
+        *         not have a signature of a known type
+        * @throws IOException
+        *             if an error occurs getting a public key
+        * @throws org.eclipse.jgit.api.errors.JGitInternalException
+        *             if signature verification fails
+        */
+       @Nullable
+       public static SignatureVerifier.SignatureVerification verify(
+                       @NonNull Repository repository, @NonNull GpgConfig config,
+                       @NonNull RevObject object) 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.nextLfSkippingSplitLines(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(repository, config, 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(repository, config, data, signatureData);
+               }
+               return null;
+       }
+
+       /**
+        * Verifies a given signature for some give data.
+        *
+        * @param repository
+        *            the {@link Repository} the object is from
+        * @param config
+        *            the {@link GpgConfig} to use
+        * @param data
+        *            to verify the signature of
+        * @param signature
+        *            the given signature of the {@code data}
+        * @return a {@link SignatureVerifier.SignatureVerification} describing the
+        *         outcome of the verification, or {@code null} if the signature
+        *         type is unknown
+        * @throws IOException
+        *             if an error occurs getting a public key
+        * @throws org.eclipse.jgit.api.errors.JGitInternalException
+        *             if signature verification fails
+        */
+       @Nullable
+       public static SignatureVerifier.SignatureVerification verify(
+                       @NonNull Repository repository, @NonNull GpgConfig config,
+                       byte[] data, byte[] signature) throws IOException {
+               GpgConfig.GpgFormat format = getFormat(signature);
+               if (format == null) {
+                       return null;
+               }
+               SignatureVerifier verifier = get(format);
+               if (verifier == null) {
+                       return null;
+               }
+               return verifier.verify(repository, config, data, signature);
+       }
+
+       /**
+        * Determines the type of a given signature.
+        *
+        * @param signature
+        *            to get the type of
+        * @return the signature type, or {@code null} if unknown
+        */
+       @Nullable
+       public static GpgConfig.GpgFormat getFormat(byte[] signature) {
+               if (RawParseUtils.match(signature, 0, PGP_PREFIX) > 0) {
+                       return GpgConfig.GpgFormat.OPENPGP;
+               }
+               if (RawParseUtils.match(signature, 0, X509_PREFIX) > 0) {
+                       return GpgConfig.GpgFormat.X509;
+               }
+               if (RawParseUtils.match(signature, 0, SSH_PREFIX) > 0) {
+                       return GpgConfig.GpgFormat.SSH;
+               }
+               return null;
+       }
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java
new file mode 100644 (file)
index 0000000..3bb7464
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * 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.io.UnsupportedEncodingException;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * Creates signatures for Git objects.
+ *
+ * @since 7.0
+ */
+public interface Signer {
+
+       /**
+        * Signs the specified object.
+        *
+        * <p>
+        * Implementors should obtain the payload for signing from the specified
+        * object via {@link ObjectBuilder#build()} and create a proper
+        * {@link GpgSignature}. The generated signature is set on the specified
+        * {@code object} (see {@link ObjectBuilder#setGpgSignature(GpgSignature)}).
+        * </p>
+        * <p>
+        * Any existing signature on the object must be discarded prior obtaining
+        * the payload via {@link ObjectBuilder#build()}.
+        * </p>
+        *
+        * @param repository
+        *            {@link Repository} the object belongs to
+        * @param config
+        *            GPG settings from the git config
+        * @param object
+        *            the object to sign (must not be {@code null} and must be
+        *            complete to allow proper calculation of payload)
+        * @param committer
+        *            the signing identity (to help with key lookup in case signing
+        *            key is not specified)
+        * @param signingKey
+        *            if non-{@code null} overrides the signing key from the config
+        * @param credentialsProvider
+        *            provider to use when querying for signing key credentials (eg.
+        *            passphrase)
+        * @throws CanceledException
+        *             when signing was canceled (eg., user aborted when entering
+        *             passphrase)
+        * @throws IOException
+        *             if an I/O error occurs
+        * @throws UnsupportedSigningFormatException
+        *             if a config is given and the wanted key format is not
+        *             supported
+        */
+       default void signObject(@NonNull Repository repository,
+                       @NonNull GpgConfig config, @NonNull ObjectBuilder object,
+                       @NonNull PersonIdent committer, String signingKey,
+                       CredentialsProvider credentialsProvider)
+                       throws CanceledException, IOException,
+                       UnsupportedSigningFormatException {
+               try {
+                       object.setGpgSignature(sign(repository, config, object.build(),
+                                       committer, signingKey, credentialsProvider));
+               } catch (UnsupportedEncodingException e) {
+                       throw new JGitInternalException(e.getMessage(), e);
+               }
+       }
+
+       /**
+        * Signs arbitrary data.
+        *
+        * @param repository
+        *            {@link Repository} the signature is created in
+        * @param config
+        *            GPG settings from the git config
+        * @param data
+        *            the data to sign
+        * @param committer
+        *            the signing identity (to help with key lookup in case signing
+        *            key is not specified)
+        * @param signingKey
+        *            if non-{@code null} overrides the signing key from the config
+        * @param credentialsProvider
+        *            provider to use when querying for signing key credentials (eg.
+        *            passphrase)
+        * @return the signature for {@code data}
+        * @throws CanceledException
+        *             when signing was canceled (eg., user aborted when entering
+        *             passphrase)
+        * @throws IOException
+        *             if an I/O error occurs
+        * @throws UnsupportedSigningFormatException
+        *             if a config is given and the wanted key format is not
+        *             supported
+        */
+       GpgSignature sign(@NonNull Repository repository, @NonNull GpgConfig config,
+                       byte[] data, @NonNull PersonIdent committer, String signingKey,
+                       CredentialsProvider credentialsProvider) throws CanceledException,
+                       IOException, UnsupportedSigningFormatException;
+
+       /**
+        * Indicates if a signing key is available for the specified committer
+        * and/or signing key.
+        *
+        * @param repository
+        *            the current {@link Repository}
+        * @param config
+        *            GPG settings from the git config
+        * @param committer
+        *            the signing identity (to help with key lookup in case signing
+        *            key is not specified)
+        * @param signingKey
+        *            if non-{@code null} overrides the signing key from the config
+        * @param credentialsProvider
+        *            provider to use when querying for signing key credentials (eg.
+        *            passphrase)
+        * @return {@code true} if a signing key is available, {@code false}
+        *         otherwise
+        * @throws CanceledException
+        *             when signing was canceled (eg., user aborted when entering
+        *             passphrase)
+        */
+       boolean canLocateSigningKey(@NonNull Repository repository,
+                       @NonNull GpgConfig config, @NonNull PersonIdent committer,
+                       String signingKey, CredentialsProvider credentialsProvider)
+                       throws CanceledException;
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java
new file mode 100644 (file)
index 0000000..125d25e
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A factory for {@link Signer}s.
+ *
+ * @since 7.0
+ */
+public interface SignerFactory {
+
+       /**
+        * Tells what kind of {@link Signer} this factory creates.
+        *
+        * @return the {@link GpgConfig.GpgFormat} of the signer
+        */
+       @NonNull
+       GpgConfig.GpgFormat getType();
+
+       /**
+        * Creates a new instance of a {@link Signer} that can produce signatures of
+        * type {@link #getType()}.
+        *
+        * @return a new {@link Signer}
+        */
+       @NonNull
+       Signer create();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java
new file mode 100644 (file)
index 0000000..7771b07
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import java.text.MessageFormat;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.JGitText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the available signers.
+ *
+ * @since 7.0
+ */
+public final class Signers {
+
+       private static final Logger LOG = LoggerFactory.getLogger(Signers.class);
+
+       private static final Map<GpgConfig.GpgFormat, SignerFactory> SIGNER_FACTORIES = loadSigners();
+
+       private static final Map<GpgConfig.GpgFormat, Signer> SIGNERS = new ConcurrentHashMap<>();
+
+       private static Map<GpgConfig.GpgFormat, SignerFactory> loadSigners() {
+               Map<GpgConfig.GpgFormat, SignerFactory> result = new EnumMap<>(
+                               GpgConfig.GpgFormat.class);
+               try {
+                       for (SignerFactory factory : ServiceLoader
+                                       .load(SignerFactory.class)) {
+                               GpgConfig.GpgFormat format = factory.getType();
+                               SignerFactory existing = result.get(format);
+                               if (existing != null) {
+                                       LOG.warn("{}", //$NON-NLS-1$
+                                                       MessageFormat.format(
+                                                                       JGitText.get().signatureServiceConflict,
+                                                                       "SignerFactory", format, //$NON-NLS-1$
+                                                                       existing.getClass().getCanonicalName(),
+                                                                       factory.getClass().getCanonicalName()));
+                               } else {
+                                       result.put(format, factory);
+                               }
+                       }
+               } catch (ServiceConfigurationError e) {
+                       LOG.error(e.getMessage(), e);
+               }
+               return result;
+       }
+
+       private Signers() {
+               // No instantiation
+       }
+
+       /**
+        * Retrieves a {@link Signer} that can produce signatures of the given type
+        * {@code format}.
+        *
+        * @param format
+        *            {@link GpgConfig.GpgFormat} the signer must support
+        * @return a {@link Signer}, or {@code null} if none is available
+        */
+       public static Signer get(@NonNull GpgConfig.GpgFormat format) {
+               return SIGNERS.computeIfAbsent(format, f -> {
+                       SignerFactory factory = SIGNER_FACTORIES.get(format);
+                       if (factory == null) {
+                               return null;
+                       }
+                       return factory.create();
+               });
+       }
+
+       /**
+        * Sets a specific signer to use for a specific signature type.
+        *
+        * @param format
+        *            signature type to set the {@code signer} for
+        * @param signer
+        *            the {@link Signer} to use for signatures of type
+        *            {@code format}; if {@code null}, a default implementation, if
+        *            available, may be used.
+        */
+       public static void set(@NonNull GpgConfig.GpgFormat format, Signer signer) {
+               if (signer == null) {
+                       SIGNERS.remove(format);
+               } else {
+                       SIGNERS.put(format, signer);
+               }
+       }
+}
index 5b50c2afd78edefbebf846346d42bfdb93ac0054..0737a7808592bc027a0630bf22b70f6e28d9117a 100644 (file)
@@ -13,7 +13,6 @@
 package org.eclipse.jgit.revwalk;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.eclipse.jgit.lib.Constants.GPG_SIGNATURE_PREFIX;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -39,8 +38,17 @@ import org.eclipse.jgit.util.StringUtils;
  */
 public class RevTag extends RevObject {
 
-       private static final byte[] hSignature = Constants
-                       .encodeASCII(GPG_SIGNATURE_PREFIX);
+       private static final byte[] SIGNATURE_START = Constants
+                       .encodeASCII("-----BEGIN"); //$NON-NLS-1$
+
+       private static final byte[] GPG_SIGNATURE_START = Constants
+                       .encodeASCII(Constants.GPG_SIGNATURE_PREFIX);
+
+       private static final byte[] CMS_SIGNATURE_START = Constants
+                       .encodeASCII(Constants.CMS_SIGNATURE_PREFIX);
+
+       private static final byte[] SSH_SIGNATURE_START = Constants
+                       .encodeASCII(Constants.SSH_SIGNATURE_PREFIX);
 
        /**
         * Parse an annotated tag from its canonical format.
@@ -209,20 +217,27 @@ public class RevTag extends RevObject {
                        return msgB;
                }
                // Find the last signature start and return the rest
-               int start = nextStart(hSignature, raw, msgB);
+               int start = nextStart(SIGNATURE_START, raw, msgB);
                if (start < 0) {
                        return start;
                }
                int next = RawParseUtils.nextLF(raw, start);
                while (next < raw.length) {
-                       int newStart = nextStart(hSignature, raw, next);
+                       int newStart = nextStart(SIGNATURE_START, raw, next);
                        if (newStart < 0) {
                                break;
                        }
                        start = newStart;
                        next = RawParseUtils.nextLF(raw, start);
                }
-               return start;
+               // SIGNATURE_START is just a prefix. Check that it is one of the known
+               // full signature start tags.
+               if (RawParseUtils.match(raw, start, GPG_SIGNATURE_START) > 0
+                               || RawParseUtils.match(raw, start, CMS_SIGNATURE_START) > 0
+                               || RawParseUtils.match(raw, start, SSH_SIGNATURE_START) > 0) {
+                       return start;
+               }
+               return -1;
        }
 
        /**
index cf06172c17cbeb6b85ac8716493700694f263d31..90524db20af0a469959d10310830e21a23cee9a5 100644 (file)
@@ -13,8 +13,8 @@ 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.SignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifier.TrustLevel;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /**
@@ -39,29 +39,31 @@ public final class SignatureUtils {
         *            to use for dates
         * @return a textual representation of the {@link SignatureVerification},
         *         using LF as line separator
+        *
+        * @since 7.0
         */
        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());
+                               verification.creationDate());
                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)));
+                               verification.keyFingerprint().toUpperCase(Locale.ROOT)));
                result.append('\n');
-               if (!StringUtils.isEmptyOrNull(verification.getSigner())) {
+               if (!StringUtils.isEmptyOrNull(verification.signer())) {
                        result.append(
                                        MessageFormat.format(JGitText.get().verifySignatureIssuer,
-                                                       verification.getSigner()));
+                                                       verification.signer()));
                        result.append('\n');
                }
                String msg;
-               if (verification.getVerified()) {
-                       if (verification.isExpired()) {
+               if (verification.verified()) {
+                       if (verification.expired()) {
                                msg = JGitText.get().verifySignatureExpired;
                        } else {
                                msg = JGitText.get().verifySignatureGood;
@@ -69,14 +71,14 @@ public final class SignatureUtils {
                } else {
                        msg = JGitText.get().verifySignatureBad;
                }
-               result.append(MessageFormat.format(msg, verification.getKeyUser()));
-               if (!TrustLevel.UNKNOWN.equals(verification.getTrustLevel())) {
+               result.append(MessageFormat.format(msg, verification.keyUser()));
+               if (!TrustLevel.UNKNOWN.equals(verification.trustLevel())) {
                        result.append(' ' + MessageFormat
                                        .format(JGitText.get().verifySignatureTrust, verification
-                                                       .getTrustLevel().name().toLowerCase(Locale.ROOT)));
+                                                       .trustLevel().name().toLowerCase(Locale.ROOT)));
                }
                result.append('\n');
-               msg = verification.getMessage();
+               msg = verification.message();
                if (!StringUtils.isEmptyOrNull(msg)) {
                        result.append(msg);
                        result.append('\n');