Merge branch 'stable-7.0' into stable-7.1

* stable-7.0:
  Do not always refresh packed-refs during ref updates

Change-Id: I6f249ceb085105ae3309c6d1312ffa9482569fb6
diff --git a/Documentation/config-options.md b/Documentation/config-options.md
index b30b958..2904824 100644
--- a/Documentation/config-options.md
+++ b/Documentation/config-options.md
@@ -31,6 +31,7 @@
 | `core.dfs.blockSize` | `64 kiB` | ⃞ | Size in bytes of a single window read in from the pack file into the DFS block cache. |
 | `core.dfs.concurrencyLevel` | `32` | ⃞ | The estimated number of threads concurrently accessing the DFS block cache. |
 | `core.dfs.deltaBaseCacheLimit` | `10 MiB` | ⃞ | Maximum number of bytes to hold in per-reader DFS delta base cache. |
+| `core.dfs.loadRevIndexInParallel` | false; | ⃞ | Try to load the reverse index in parallel with the bitmap index. |
 | `core.dfs.streamFileThreshold` | `50 MiB` | ⃞ | The size threshold beyond which objects must be streamed. |
 | `core.dfs.streamBuffer` | Block size of the pack | ⃞ | Number of bytes to use for buffering when streaming a pack file during copying. If 0 the block size of the pack is used|
 | `core.dfs.streamRatio` | `0.30` | ⃞ | Ratio of DFS block cache to occupy with a copied pack. Values between `0` and `1.0`. |
diff --git a/WORKSPACE b/WORKSPACE
index 34caf9d..e1adb8a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -122,18 +122,18 @@
     sha1 = "c070ac920e72023ae9ab0a3f3a866bece284b470",
 )
 
-JNA_VERS = "5.14.0"
+JNA_VERS = "5.15.0"
 
 maven_jar(
     name = "jna",
     artifact = "net.java.dev.jna:jna:" + JNA_VERS,
-    sha1 = "67bf3eaea4f0718cb376a181a629e5f88fa1c9dd",
+    sha1 = "01ee1d80ff44f08280188f7c0e740d57207841ac",
 )
 
 maven_jar(
     name = "jna-platform",
     artifact = "net.java.dev.jna:jna-platform:" + JNA_VERS,
-    sha1 = "28934d48aed814f11e4c584da55c49fa7032b31b",
+    sha1 = "86b502cad57d45da172b5e3231c537b042e296ef",
 )
 
 maven_jar(
@@ -174,14 +174,14 @@
 
 maven_jar(
     name = "commons-lang3",
-    artifact = "org.apache.commons:commons-lang3:3.16.0",
-    sha1 = "3eb54effe40946dfb06dc5cd6c7ce4116cd51ea4",
+    artifact = "org.apache.commons:commons-lang3:3.17.0",
+    sha1 = "b17d2136f0460dcc0d2016ceefca8723bdf4ee70",
 )
 
 maven_jar(
     name = "commons-io",
-    artifact = "commons-io:commons-io:2.16.1",
-    sha1 = "377d592e740dc77124e0901291dbfaa6810a200e",
+    artifact = "commons-io:commons-io:2.17.0",
+    sha1 = "ddcc8433eb019fb48fe25207c0278143f3e1d7e2",
 )
 
 maven_jar(
@@ -210,8 +210,8 @@
 
 maven_jar(
     name = "mockito",
-    artifact = "org.mockito:mockito-core:5.12.0",
-    sha1 = "22f8bbaf478e6789164787fa411a3b5ed986e110",
+    artifact = "org.mockito:mockito-core:5.14.2",
+    sha1 = "f7bf936008d7664e2002c3faf0c02071c8d10e7c",
 )
 
 maven_jar(
@@ -220,18 +220,18 @@
     sha1 = "0d26263eb7524252d98e602fc6942996a3195e29",
 )
 
-BYTE_BUDDY_VERSION = "1.15.0"
+BYTE_BUDDY_VERSION = "1.15.10"
 
 maven_jar(
     name = "bytebuddy",
     artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
-    sha1 = "a5b1159b91c5334015de0f22ab4b1188cd42bbff",
+    sha1 = "635c873fadd853c084f84fdc3cbd58c5dd8537f9",
 )
 
 maven_jar(
     name = "bytebuddy-agent",
     artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
-    sha1 = "e32740c43acebaac9d55b86399ecf6a5df3c17fb",
+    sha1 = "0e8eb255b2c378b9a6c7341e7b0e12f0a5636377",
 )
 
 maven_jar(
@@ -246,90 +246,82 @@
     sha1 = "527175ca6d81050b53bdd4c457a6d6e017626b0e",
 )
 
-JETTY_VER = "12.0.12"
+JETTY_VER = "12.0.15"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty.ee10:jetty-ee10-servlet:" + JETTY_VER,
-    sha1 = "12f25f260a8f9fb519b6d3058260564277e618cd",
-    src_sha1 = "16a22f6ed585c6dcab07d111de290301373db1c7",
+    sha1 = "a9362717fa1756f9f1f18e0ef2ce671e742b7afb",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VER,
-    sha1 = "962d2c5748f750aae667399481915bd02a9746e0",
-    src_sha1 = "e953d0e19e6e420f5e7c1f62424c4bbe0d385d15",
+    sha1 = "e15efd84ed53277f8e893574edaed4734c161f44",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VER,
-    sha1 = "77591ab10113d7de7eb8a01bb4d9d23b234aebd1",
-    src_sha1 = "952873398a59bc5a964d9d2d130227af807559e9",
+    sha1 = "2bd3742c6831e42c6ebfa9c990386a8dca71dee8",
 )
 
 maven_jar(
     name = "jetty-session",
     artifact = "org.eclipse.jetty:jetty-session:" + JETTY_VER,
-    sha1 = "fceae460fb4677a6cec80d3b70b1fc3ae2a114a7",
-    src_sha1 = "ba95d5e17a3d7f09ca908d4c0773e913f3612425",
+    sha1 = "2819021282ff2f7fbaa53feb2fe063130bd4613c",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VER,
-    sha1 = "549fe58ae50b9c061d09803fb0d2659a93ce0ecd",
-    src_sha1 = "c7afad980b9eade12b473172a9d069d96d016677",
+    sha1 = "a36fcfde8316b374102c5b43d7247ec501e906d8",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VER,
-    sha1 = "4e2cd5c23e8ba550238f35c361c22ffb1e7bf00c",
-    src_sha1 = "42397797c496a51520900a529ba7e1b315aba768",
+    sha1 = "e1657f842a0e362171a8a41f35d85cdba1a47872",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VER,
-    sha1 = "f7b1e4f835c38d12668b426d327118e767d2f1d4",
-    src_sha1 = "0fdb95a123cc50f6adf4154edc5316cabca05f58",
+    sha1 = "9fbd3ac58607af034ad065b366798798bb5f7b5e",
 )
 
 maven_jar(
     name = "jetty-util-ajax",
     artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VER,
-    sha1 = "330e692032c82c305cd52c6a8e9bfba0f1da2555",
-    src_sha1 = "d6b9274b3ef0419a6ba869c828fd0c59c570cd9d",
+    sha1 = "d5c2f0ea177c5c178874101c186cc9729cf7ea92",
 )
 
-BOUNCYCASTLE_VER = "1.78.1"
+BOUNCYCASTLE_VER = "1.79"
 
 maven_jar(
     name = "bcpg",
     artifact = "org.bouncycastle:bcpg-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "6c8dbcec20355278ec54840e735f63db2479150e",
-    src_sha1 = "2ddef60d84dd8c14ebce4c13100f0bc55fed6922",
+    sha1 = "904dd8a8e1c9f7d58d1ffa7f4ca3fb00736a601f",
+    src_sha1 = "9e372826141edb213d5921131ee68dc276dc99ef",
 )
 
 maven_jar(
     name = "bcprov",
     artifact = "org.bouncycastle:bcprov-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "39e9e45359e20998eb79c1828751f94a818d25f8",
-    src_sha1 = "70f58ec93da543dda6a21614b768cb2e386fd512",
+    sha1 = "4d8e2732bcee15f1db93df266c3f5b70ce5cac21",
+    src_sha1 = "8647816d667ee526a8e3a456229ac5f9f96d2315",
 )
 
 maven_jar(
     name = "bcutil",
     artifact = "org.bouncycastle:bcutil-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "5353ca39fe2f148dab9ca1d637a43d0750456254",
-    src_sha1 = "8d2e0747f5d806f39a602f7f91610444d88c4e2c",
+    sha1 = "ecfc5aef97cc7676ea0de5c53c407b9f533f0ad5",
+    src_sha1 = "00df03977fb0b80395da655623abca9d7d7dcb66",
 )
 
 maven_jar(
     name = "bcpkix",
     artifact = "org.bouncycastle:bcpkix-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "17b3541f736df97465f87d9f5b5dfa4991b37bb3",
-    src_sha1 = "3aeaf221772ad0c9c04593688cb86c6eb74d48b9",
+    sha1 = "7693cec3b8779b74b35466dcaeeaac7409872954",
+    src_sha1 = "57a60d1d9f75320eef70a095dfae679d97ade1c2",
 )
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
index 251a2f7..3644186 100644
--- a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
@@ -5,13 +5,13 @@
 Automatic-Module-Name: org.eclipse.jgit.ant.test
 Bundle-SymbolicName: org.eclipse.jgit.ant.test
 Bundle-Vendor: %Bundle-Vendor
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: org.apache.tools.ant,
- org.eclipse.jgit.ant.tasks;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.ant.tasks;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)"
diff --git a/org.eclipse.jgit.ant.test/pom.xml b/org.eclipse.jgit.ant.test/pom.xml
index e32e54f..a145760 100644
--- a/org.eclipse.jgit.ant.test/pom.xml
+++ b/org.eclipse.jgit.ant.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant.test</artifactId>
diff --git a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
index b59b2be..b22bdb9 100644
--- a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
@@ -3,13 +3,13 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ant
 Bundle-SymbolicName: org.eclipse.jgit.ant
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: org.apache.tools.ant,
-  org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)"
+  org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)"
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.ant;version="7.0.2",
- org.eclipse.jgit.ant.tasks;version="7.0.2";
+Export-Package: org.eclipse.jgit.ant;version="7.1.2",
+ org.eclipse.jgit.ant.tasks;version="7.1.2";
   uses:="org.apache.tools.ant,
    org.apache.tools.ant.types"
diff --git a/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF
index 2a78006..9fb41bb 100644
--- a/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ant - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ant.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ant;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ant;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ant/pom.xml b/org.eclipse.jgit.ant/pom.xml
index b60743d..0376cef 100644
--- a/org.eclipse.jgit.ant/pom.xml
+++ b/org.eclipse.jgit.ant/pom.xml
@@ -15,7 +15,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant</artifactId>
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index 1318c44..c9d4352 100644
--- a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.archive
 Bundle-SymbolicName: org.eclipse.jgit.archive
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -13,18 +13,18 @@
  org.apache.commons.compress.compressors.bzip2;version="[1.4,2.0)",
  org.apache.commons.compress.compressors.gzip;version="[1.4,2.0)",
  org.apache.commons.compress.compressors.xz;version="[1.4,2.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.osgi.framework;version="[1.3.0,2.0.0)",
  org.tukaani.xz
 Bundle-ActivationPolicy: lazy
 Bundle-Activator: org.eclipse.jgit.archive.FormatActivator
-Export-Package: org.eclipse.jgit.archive;version="7.0.2";
+Export-Package: org.eclipse.jgit.archive;version="7.1.2";
   uses:="org.apache.commons.compress.archivers,
    org.osgi.framework,
    org.eclipse.jgit.api,
    org.eclipse.jgit.lib",
- org.eclipse.jgit.archive.internal;version="7.0.2";x-internal:=true
+ org.eclipse.jgit.archive.internal;version="7.1.2";x-internal:=true
diff --git a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
index 95915c2..9d11d98 100644
--- a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.archive - Sources
 Bundle-SymbolicName: org.eclipse.jgit.archive.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.archive;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.archive;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.archive/pom.xml b/org.eclipse.jgit.archive/pom.xml
index aa1e770..f557b01 100644
--- a/org.eclipse.jgit.archive/pom.xml
+++ b/org.eclipse.jgit.archive/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.archive</artifactId>
diff --git a/org.eclipse.jgit.benchmarks/pom.xml b/org.eclipse.jgit.benchmarks/pom.xml
index dbf35a3..60f83cb 100644
--- a/org.eclipse.jgit.benchmarks/pom.xml
+++ b/org.eclipse.jgit.benchmarks/pom.xml
@@ -16,7 +16,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.benchmarks</artifactId>
diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/RawTextBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/RawTextBenchmark.java
new file mode 100644
index 0000000..19297eb
--- /dev/null
+++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/RawTextBenchmark.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2022, Matthias Sohn <matthias.sohn@sap.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.benchmarks;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.eclipse.jgit.diff.RawText.getBufferSize;
+import static org.eclipse.jgit.diff.RawText.isBinary;
+import static org.eclipse.jgit.diff.RawText.isCrLfText;
+
+@State(Scope.Thread)
+public class RawTextBenchmark {
+
+	@State(Scope.Benchmark)
+	public static class BenchmarkState {
+
+		@Param({"1", "2", "3", "4", "5", "6"})
+		int testIndex;
+
+		@Param({"false", "true"})
+		boolean complete;
+
+		byte[] bytes;
+
+		@Setup
+		public void setupBenchmark() {
+			switch (testIndex) {
+				case 1: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					bytes = tmpBytes;
+					break;
+				}
+				case 2: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					byte[] tmpBytes2 = new byte[tmpBytes.length + 1];
+					System.arraycopy(tmpBytes, 0, tmpBytes2, 0, tmpBytes.length);
+					tmpBytes2[500] = '\0';
+					tmpBytes2[tmpBytes.length] = '\0';
+					bytes = tmpBytes2;
+					break;
+				}
+				case 3: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					byte[] tmpBytes2 = new byte[tmpBytes.length + 1];
+					System.arraycopy(tmpBytes, 0, tmpBytes2, 0, tmpBytes.length);
+					tmpBytes2[500] = '\r';
+					tmpBytes2[tmpBytes.length] = '\r';
+					bytes = tmpBytes2;
+					break;
+				}
+				case 4: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					byte[] tmpBytes2 = new byte[tmpBytes.length + 1];
+					System.arraycopy(tmpBytes, 0, tmpBytes2, 0, tmpBytes.length);
+					tmpBytes2[499] = '\r';
+					tmpBytes2[500] = '\n';
+					tmpBytes2[tmpBytes.length - 1] = '\r';
+					tmpBytes2[tmpBytes.length] = '\n';
+					bytes = tmpBytes2;
+					break;
+				}
+				case 5: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					tmpBytes[0] = '\0';
+					bytes = tmpBytes;
+					break;
+				}
+				case 6: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					tmpBytes[0] = '\r';
+					bytes = tmpBytes;
+					break;
+				}
+				default:
+			}
+		}
+
+		@TearDown
+		public void teardown() {
+		}
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextOld(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextOld(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNewCandidate1(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextNewCandidate1(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNewCandidate2(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextNewCandidate2(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNewCandidate3(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextNewCandidate3(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNew(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfText(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsBinaryOld(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isBinaryOld(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsBinaryNew(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isBinary(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+
+	/**
+	 * Determine heuristically whether a byte array represents binary (as
+	 * opposed to text) content.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate. This should be
+	 *			{@code raw.length} unless {@code raw} was over-allocated by
+	 *			the caller.
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @return true if raw is likely to be a binary file, false otherwise
+	 * @since 6.0
+	 */
+	public static boolean isBinaryOld(byte[] raw, int length, boolean complete) {
+		// Similar heuristic as C Git. Differences:
+		// - limited buffer size; may be only the beginning of a large blob
+		// - no counting of printable vs. non-printable bytes < 0x20 and 0x7F
+		int maxLength = getBufferSize();
+		boolean isComplete = complete;
+		if (length > maxLength) {
+			// We restrict the length in all cases to getBufferSize() to get
+			// predictable behavior. Sometimes we load streams, and sometimes we
+			// have the full data in memory. With streams, we never look at more
+			// than the first getBufferSize() bytes. If we looked at more when
+			// we have the full data, different code paths in JGit might come to
+			// different conclusions.
+			length = maxLength;
+			isComplete = false;
+		}
+		byte last = 'x'; // Just something inconspicuous.
+		for (int ptr = 0; ptr < length; ptr++) {
+			byte curr = raw[ptr];
+			if (isBinary(curr, last)) {
+				return true;
+			}
+			last = curr;
+		}
+		if (isComplete) {
+			// Buffer contains everything...
+			return last == '\r'; // ... so this must be a lone CR
+		}
+		return false;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw	  the raw file content.
+	 * @param length   number of bytes in {@code raw} to evaluate.
+	 * @param complete whether {@code raw} contains the whole data
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 * {@code false} otherwise
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextOld(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+		byte last = 'x'; // Just something inconspicuous
+		for (int ptr = 0; ptr < length; ptr++) {
+			byte curr = raw[ptr];
+			if (isBinary(curr, last)) {
+				return false;
+			}
+			if (curr == '\n' && last == '\r') {
+				has_crlf = true;
+			}
+			last = curr;
+		}
+		if (last == '\r') {
+			if (complete) {
+				// Lone CR: it's binary after all.
+				return false;
+			}
+			// Tough call. If the next byte, which we don't have, would be a
+			// '\n', it'd be a CR-LF text, otherwise it'd be binary. Just decide
+			// based on what we already scanned; it wasn't binary until now.
+		}
+		return has_crlf;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate.
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 *		 {@code false} otherwise
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextNewCandidate1(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+
+		// first detect empty
+		if (length <= 0) {
+			return false;
+		}
+
+		// next detect '\0'
+		for (int reversePtr = length - 1; reversePtr >= 0; --reversePtr) {
+			if (raw[reversePtr] == '\0') {
+				return false;
+			}
+		}
+
+		// if '\r' be last, then if complete then return non-crlf
+		if (raw[length - 1] == '\r' && complete) {
+			return false;
+		}
+
+		for (int ptr = 0; ptr < length - 1; ptr++) {
+			byte curr = raw[ptr];
+			if (curr == '\r') {
+				byte next = raw[ptr + 1];
+				if (next != '\n') {
+					return false;
+				}
+				// else
+				// we have crlf here
+				has_crlf = true;
+				// as next is '\n', it can never be '\r', just skip it from next check
+				++ptr;
+			}
+		}
+
+		return has_crlf;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate.
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 *		 {@code false} otherwise
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextNewCandidate2(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+
+		// first detect empty
+		if (length <= 0) {
+			return false;
+		}
+
+		// if '\r' be last, then if complete then return non-crlf
+		byte last = raw[length - 1];
+		if (last == '\0' || last == '\r' && complete) {
+			return false;
+		}
+
+		for (int ptr = 0; ptr < length - 1; ptr++) {
+			byte b = raw[ptr];
+			switch (b) {
+				case  '\0':
+					return false;
+				case '\r': {
+					++ptr;
+					b = raw[ptr];
+					if (b != '\n') {
+						return false;
+					}
+					// else
+					// we have crlf here
+					has_crlf = true;
+					// as next is '\n', it can never be '\r', just skip it from next check
+					break;
+				}
+				default:
+					// do nothing;
+					break;
+			}
+		}
+
+		return has_crlf;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate.
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 *		 {@code false} otherwise
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextNewCandidate3(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+
+		int ptr = -1;
+		byte current;
+		while (ptr < length - 2) {
+			current = raw[++ptr];
+			if ('\0' == current || '\r' == current && (raw[++ptr] != '\n' || !(has_crlf = true))) {
+				return false;
+			}
+		}
+
+		if (ptr == length - 2) {
+			// if '\r' be last, then if isComplete then return binary
+			current = raw[++ptr];
+			if('\0' == current || '\r' == current && complete){
+				return false;
+			}
+		}
+
+		return has_crlf;
+	}
+
+
+	public static void main(String[] args) throws RunnerException {
+		Options opt = new OptionsBuilder()
+				.include(RawTextBenchmark.class.getSimpleName())
+				.forks(1).jvmArgs("-ea").build();
+		new Runner(opt).run();
+	}
+}
diff --git a/org.eclipse.jgit.coverage/pom.xml b/org.eclipse.jgit.coverage/pom.xml
index 499c0de..da71786 100644
--- a/org.eclipse.jgit.coverage/pom.xml
+++ b/org.eclipse.jgit.coverage/pom.xml
@@ -14,7 +14,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -27,88 +27,88 @@
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.archive</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.apache</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.server</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ui</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
 
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
index 707c55b..c934a4f 100644
--- a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
@@ -3,19 +3,20 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.gpg.bc.test
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
-Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
- org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
- org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
- org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
- org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
- org.eclipse.jgit.gpg.bc.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.gpg.bc.internal.keys;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.sha1;version="[7.0.2,7.1.0)",
+Import-Package: org.bouncycastle.asn1.cryptlib;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jce.provider;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.79.0,2.0.0)",
+ org.eclipse.jgit.gpg.bc.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.sha1;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
  org.junit.runners;version="[4.13,5.0.0)"
diff --git a/org.eclipse.jgit.gpg.bc.test/pom.xml b/org.eclipse.jgit.gpg.bc.test/pom.xml
index 2310b6f..d821f19 100644
--- a/org.eclipse.jgit.gpg.bc.test/pom.xml
+++ b/org.eclipse.jgit.gpg.bc.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc.test</artifactId>
diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
index fed0610..d486c97 100644
--- a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
+++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
@@ -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,10 +9,7 @@
  */
 package org.eclipse.jgit.gpg.bc.internal.keys;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import java.io.BufferedInputStream;
 import java.io.IOException;
@@ -20,8 +17,6 @@
 import java.security.Security;
 import java.util.Iterator;
 
-import javax.crypto.Cipher;
-
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPublicKey;
@@ -49,39 +44,15 @@ public static void ensureBC() {
 		}
 	}
 
-	private static volatile Boolean haveOCB;
-
-	private static boolean ocbAvailable() {
-		Boolean haveIt = haveOCB;
-		if (haveIt != null) {
-			return haveIt.booleanValue();
-		}
-		try {
-			Cipher c = Cipher.getInstance("AES/OCB/NoPadding"); //$NON-NLS-1$
-			if (c == null) {
-				haveOCB = Boolean.FALSE;
-				return false;
-			}
-		} catch (NoClassDefFoundError | Exception e) {
-			haveOCB = Boolean.FALSE;
-			return false;
-		}
-		haveOCB = Boolean.TRUE;
-		return true;
-	}
-
 	private static class TestData {
 
 		final String name;
 
 		final boolean encrypted;
 
-		final boolean keyValue;
-
-		TestData(String name, boolean encrypted, boolean keyValue) {
+		TestData(String name, boolean encrypted) {
 			this.name = name;
 			this.encrypted = encrypted;
-			this.keyValue = keyValue;
 		}
 
 		@Override
@@ -93,19 +64,12 @@ public String toString() {
 	@Parameters(name = "{0}")
 	public static TestData[] initTestData() {
 		return new TestData[] {
-				new TestData("AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11", false, false),
-				new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false, true),
-				new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true, true),
-				new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true, true),
-				new TestData("62D43D7F117F7A5E4998ECB6617EE9942D069C14", true, true),
-				new TestData("faked", false, true) };
-	}
-
-	private static byte[] readTestKey(String filename) throws Exception {
-		try (InputStream in = new BufferedInputStream(
-				SecretKeysTest.class.getResourceAsStream(filename))) {
-			return SecretKeys.keyFromNameValueFormat(in);
-		}
+				new TestData("AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11", false),
+				new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false),
+				new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true),
+				new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true),
+				new TestData("62D43D7F117F7A5E4998ECB6617EE9942D069C14", true),
+				new TestData("faked", false) };
 	}
 
 	private static PGPPublicKey readAsc(InputStream in)
@@ -131,11 +95,6 @@ private static PGPPublicKey readAsc(InputStream in)
 
 	@Test
 	public void testKeyRead() throws Exception {
-		if (data.keyValue) {
-			byte[] bytes = readTestKey(data.name + ".key");
-			assertEquals('(', bytes[0]);
-			assertEquals(')', bytes[bytes.length - 1]);
-		}
 		try (InputStream pubIn = this.getClass()
 				.getResourceAsStream(data.name + ".asc")) {
 			if (pubIn != null) {
@@ -151,11 +110,6 @@ public void testKeyRead() throws Exception {
 									: null,
 							publicKey);
 					assertNotNull(secretKey);
-				} catch (PGPException e) {
-					// Currently we may not be able to load OCB-encrypted keys.
-					assertTrue(e.toString(), e.getMessage().contains("OCB"));
-					assertTrue(data.encrypted);
-					assertFalse(ocbAvailable());
 				}
 			}
 		}
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index 6bed05d..77fd134 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -3,31 +3,28 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.gpg.bc
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc;singleton:=true
-Fragment-Host: org.eclipse.jgit;bundle-version="[7.0.2,7.1.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[7.1.2,7.2.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/gpg_bc
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.bouncycastle.asn1;version="[1.69.0,2.0.0)",
- org.bouncycastle.asn1.x9;version="[1.69.0,2.0.0)",
- org.bouncycastle.bcpg;version="[1.69.0,2.0.0)",
- org.bouncycastle.bcpg.sig;version="[1.69.0,2.0.0)",
- org.bouncycastle.crypto.ec;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg.keybox;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg.keybox.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.jcajce.interfaces;version="[1.69.0,2.0.0)",
- org.bouncycastle.jcajce.util;version="[1.69.0,2.0.0)",
- org.bouncycastle.jce.provider;version="[1.69.0,2.0.0)",
- org.bouncycastle.math.ec;version="[1.69.0,2.0.0)",
- org.bouncycastle.math.field;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.operator;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.operator.jcajce;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)",
+Import-Package: org.bouncycastle.asn1;version="[1.79.0,2.0.0)",
+ org.bouncycastle.asn1.x9;version="[1.79.0,2.0.0)",
+ org.bouncycastle.bcpg;version="[1.79.0,2.0.0)",
+ org.bouncycastle.bcpg.sig;version="[1.79.0,2.0.0)",
+ org.bouncycastle.crypto.ec;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg.keybox;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg.keybox.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jcajce.interfaces;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jcajce.util;version="[1.79.0,2.0.0)",
+ org.bouncycastle.math.ec;version="[1.79.0,2.0.0)",
+ org.bouncycastle.math.field;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.79.0,2.0.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc.internal;version="7.0.2";x-friends:="org.eclipse.jgit.gpg.bc.test",
- org.eclipse.jgit.gpg.bc.internal.keys;version="7.0.2";x-friends:="org.eclipse.jgit.gpg.bc.test"
+Export-Package: org.eclipse.jgit.gpg.bc.internal;version="7.1.2";x-friends:="org.eclipse.jgit.gpg.bc.test",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="7.1.2";x-friends:="org.eclipse.jgit.gpg.bc.test"
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
index 9dc9a9f..e8d547c 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.gpg.bc - Sources
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.gpg.bc/about.html b/org.eclipse.jgit.gpg.bc/about.html
index fc527d5..92b9409 100644
--- a/org.eclipse.jgit.gpg.bc/about.html
+++ b/org.eclipse.jgit.gpg.bc/about.html
@@ -58,32 +58,6 @@
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.</p>
 
-<hr>
-<p><b>org.eclipse.jgit.gpg.bc.internal.keys.SExprParser - MIT</b></p>
-
-<p>Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc.
-(<a href="https://www.bouncycastle.org">https://www.bouncycastle.org</a>)</p>
-
-<p>
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software
-and associated documentation files (the "Software"), to deal in the Software without restriction,
-including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
-and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-</p>
-<p>
-The above copyright notice and this permission notice shall be included in all copies or substantial
-portions of the Software.
-</p>
-<p>
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
-</p>
-
 </body>
 
 </html>
diff --git a/org.eclipse.jgit.gpg.bc/pom.xml b/org.eclipse.jgit.gpg.bc/pom.xml
index f51b978..e4c535d 100644
--- a/org.eclipse.jgit.gpg.bc/pom.xml
+++ b/org.eclipse.jgit.gpg.bc/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc</artifactId>
diff --git a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
index 77ca2cd..9e7f98c 100644
--- a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
+++ b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
@@ -1,7 +1,5 @@
 corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
 credentialPassphrase=Passphrase
-cryptCipherError=Cannot create cipher to decrypt: {0}
-cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes
 gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
 gpgNoCredentialsProvider=missing credentials provider
 gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
@@ -9,22 +7,14 @@
 gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
 gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
 gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: {0}
-gpgNoSuchAlgorithm=Cannot decrypt encrypted secret key: encryption algorithm {0} is not available
 gpgNotASigningKey=Secret key ({0}) is not suitable for signing
 gpgKeyInfo=GPG Key (fingerprint {0})
 gpgSigningCancelled=Signing was cancelled
+keyAlgorithmMismatch=Secret key has a different algorithm than the public key
+keyMismatch=Secret key does not match public key; public key is {0} {1} while secret key is for {2} {3}
 logWarnGnuPGHome=Cannot access GPG home directory given by environment variable GNUPGHOME={}
 logWarnGpgHomeProperty=Cannot access GPG home directory given by Java system property jgit.gpg.home={}
 nonSignatureError=Signature does not decode into a signature object
-secretKeyTooShort=Secret key file corrupt; only {0} bytes read
-sexprHexNotClosed=Hex number in s-expression not closed
-sexprHexOdd=Hex number in s-expression has an odd number of digits
-sexprStringInvalidEscape=Invalid escape {0} in s-expression
-sexprStringInvalidEscapeAtEnd=Invalid s-expression: quoted string ends with escape character
-sexprStringInvalidHexEscape=Invalid hex escape in s-expression
-sexprStringInvalidOctalEscape=Invalid octal escape in s-expression
-sexprStringNotClosed=String in s-expression not closed
-sexprUnhandled=Unhandled token {0} in s-expression
 signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1}
 signatureKeyLookupError=Error occurred while looking for public key
 signatureNoKeyInfo=No way to determine a public key from the signature
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
index 705e195..fcae7c2 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
@@ -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
@@ -30,8 +30,6 @@ public static BCText get() {
 	// @formatter:off
 	/***/ public String corrupt25519Key;
 	/***/ public String credentialPassphrase;
-	/***/ public String cryptCipherError;
-	/***/ public String cryptWrongDecryptedLength;
 	/***/ public String gpgFailedToParseSecretKey;
 	/***/ public String gpgNoCredentialsProvider;
 	/***/ public String gpgNoKeygrip;
@@ -39,22 +37,14 @@ public static BCText get() {
 	/***/ public String gpgNoKeyInLegacySecring;
 	/***/ public String gpgNoPublicKeyFound;
 	/***/ public String gpgNoSecretKeyForPublicKey;
-	/***/ public String gpgNoSuchAlgorithm;
 	/***/ public String gpgNotASigningKey;
 	/***/ public String gpgKeyInfo;
 	/***/ public String gpgSigningCancelled;
+	/***/ public String keyAlgorithmMismatch;
+	/***/ public String keyMismatch;
 	/***/ public String logWarnGnuPGHome;
 	/***/ public String logWarnGpgHomeProperty;
 	/***/ public String nonSignatureError;
-	/***/ public String secretKeyTooShort;
-	/***/ public String sexprHexNotClosed;
-	/***/ public String sexprHexOdd;
-	/***/ public String sexprStringInvalidEscape;
-	/***/ public String sexprStringInvalidEscapeAtEnd;
-	/***/ public String sexprStringInvalidHexEscape;
-	/***/ public String sexprStringInvalidOctalEscape;
-	/***/ public String sexprStringNotClosed;
-	/***/ public String sexprUnhandled;
 	/***/ public String signatureInconsistent;
 	/***/ public String signatureKeyLookupError;
 	/***/ public String signatureNoKeyInfo;
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
index 1d187a5..adac9b1 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
@@ -105,7 +105,8 @@ public GpgSignature sign(Repository repository, GpgConfig config,
 			PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
 					new JcaPGPContentSignerBuilder(
 							publicKey.getAlgorithm(),
-							HashAlgorithmTags.SHA256));
+							HashAlgorithmTags.SHA256),
+					publicKey);
 			signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
 			PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
 			subpackets.setIssuerFingerprint(false, publicKey);
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
deleted file mode 100644
index 3924d68..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
+++ /dev/null
@@ -1,125 +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.internal.keys;
-
-import java.security.NoSuchAlgorithmException;
-import java.text.MessageFormat;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
-import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
-import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
-import org.bouncycastle.util.Arrays;
-import org.eclipse.jgit.gpg.bc.internal.BCText;
-
-/**
- * A {@link PBEProtectionRemoverFactory} using AES/OCB/NoPadding for decryption.
- * It accepts an AAD in the factory's constructor, so the factory can be used to
- * create a {@link PBESecretKeyDecryptor} only for a particular input.
- * <p>
- * For JGit's needs, this is sufficient, but for a general upstream
- * implementation that limitation might not be acceptable.
- * </p>
- */
-class OCBPBEProtectionRemoverFactory
-		implements PBEProtectionRemoverFactory {
-
-	private final PGPDigestCalculatorProvider calculatorProvider;
-
-	private final char[] passphrase;
-
-	private final byte[] aad;
-
-	/**
-	 * Creates a new factory instance with the given parameters.
-	 * <p>
-	 * Because the AAD is given at factory level, the {@link PBESecretKeyDecryptor}s
-	 * created by the factory can be used to decrypt only a particular input
-	 * matching this AAD.
-	 * </p>
-	 *
-	 * @param passphrase         to use for secret key derivation
-	 * @param calculatorProvider for computing digests
-	 * @param aad                for the OCB decryption
-	 */
-	OCBPBEProtectionRemoverFactory(char[] passphrase,
-			PGPDigestCalculatorProvider calculatorProvider, byte[] aad) {
-		this.calculatorProvider = calculatorProvider;
-		this.passphrase = passphrase;
-		this.aad = aad;
-	}
-
-	@Override
-	public PBESecretKeyDecryptor createDecryptor(String protection)
-			throws PGPException {
-		return new PBESecretKeyDecryptor(passphrase, calculatorProvider) {
-
-			@Override
-			public byte[] recoverKeyData(int encAlgorithm, byte[] key,
-					byte[] iv, byte[] encrypted, int encryptedOffset,
-					int encryptedLength) throws PGPException {
-				String algorithmName = PGPUtil
-						.getSymmetricCipherName(encAlgorithm);
-				byte[] decrypted = null;
-				try {
-					// errorprone: "Dynamically constructed transformation
-					// strings are also flagged, as they may conceal an instance
-					// of ECB mode."
-					@SuppressWarnings("InsecureCryptoUsage")
-					Cipher c = Cipher
-							.getInstance(algorithmName + "/OCB/NoPadding"); //$NON-NLS-1$
-					SecretKey secretKey = new SecretKeySpec(key, algorithmName);
-					c.init(Cipher.DECRYPT_MODE, secretKey,
-							new IvParameterSpec(iv));
-					c.updateAAD(aad);
-					decrypted = new byte[c.getOutputSize(encryptedLength)];
-					int decryptedLength = c.update(encrypted, encryptedOffset,
-							encryptedLength, decrypted);
-					// doFinal() for OCB will check the MAC and throw an
-					// exception if it doesn't match
-					decryptedLength += c.doFinal(decrypted, decryptedLength);
-					if (decryptedLength != decrypted.length) {
-						throw new PGPException(MessageFormat.format(
-								BCText.get().cryptWrongDecryptedLength,
-								Integer.valueOf(decryptedLength),
-								Integer.valueOf(decrypted.length)));
-					}
-					byte[] result = decrypted;
-					decrypted = null; // Don't clear in finally
-					return result;
-				} catch (NoClassDefFoundError e) {
-					String msg = MessageFormat.format(
-							BCText.get().gpgNoSuchAlgorithm,
-							algorithmName + "/OCB"); //$NON-NLS-1$
-					throw new PGPException(msg,
-							new NoSuchAlgorithmException(msg, e));
-				} catch (PGPException e) {
-					throw e;
-				} catch (Exception e) {
-					throw new PGPException(
-							MessageFormat.format(BCText.get().cryptCipherError,
-									e.getLocalizedMessage()),
-							e);
-				} finally {
-					if (decrypted != null) {
-						// Prevent halfway decrypted data leaking.
-						Arrays.fill(decrypted, (byte) 0);
-					}
-				}
-			}
-		};
-	}
-}
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
deleted file mode 100644
index fd030ee..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
+++ /dev/null
@@ -1,859 +0,0 @@
-/*
- * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
- * <p>
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
- * and associated documentation files (the "Software"), to deal in the Software without restriction,
- *including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- * </p>
- * <p>
- * The above copyright notice and this permission notice shall be included in all copies or substantial
- * portions of the Software.
- * </p>
- * <p>
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
- * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
- * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- * </p>
- */
-package org.eclipse.jgit.gpg.bc.internal.keys;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.math.BigInteger;
-import java.util.Date;
-
-import org.bouncycastle.asn1.ASN1ObjectIdentifier;
-import org.bouncycastle.asn1.x9.ECNamedCurveTable;
-import org.bouncycastle.bcpg.DSAPublicBCPGKey;
-import org.bouncycastle.bcpg.DSASecretBCPGKey;
-import org.bouncycastle.bcpg.ECDSAPublicBCPGKey;
-import org.bouncycastle.bcpg.ECPublicBCPGKey;
-import org.bouncycastle.bcpg.ECSecretBCPGKey;
-import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
-import org.bouncycastle.bcpg.ElGamalSecretBCPGKey;
-import org.bouncycastle.bcpg.HashAlgorithmTags;
-import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
-import org.bouncycastle.bcpg.PublicKeyPacket;
-import org.bouncycastle.bcpg.RSAPublicBCPGKey;
-import org.bouncycastle.bcpg.RSASecretBCPGKey;
-import org.bouncycastle.bcpg.S2K;
-import org.bouncycastle.bcpg.SecretKeyPacket;
-import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
-import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPPublicKey;
-import org.bouncycastle.openpgp.PGPSecretKey;
-import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
-import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
-import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
-import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
-import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
-import org.bouncycastle.util.Arrays;
-import org.bouncycastle.util.Strings;
-
-/**
- * A parser for secret keys stored in s-expressions. Original BouncyCastle code
- * modified by the JGit team to:
- * <ul>
- * <li>handle unencrypted DSA, EC, and ElGamal keys (upstream only handles
- * unencrypted RSA)</li>
- * <li>handle secret keys using AES/OCB as encryption (those don't have a
- * hash)</li>
- * <li>fix EC parsing to account for "flags" sub-list present for ed25519 and
- * curve25519</li>
- * <li>add support for ed25519 OIDs unknown to BouncyCastle</li>
- * </ul>
- */
-@SuppressWarnings("nls")
-public class SExprParser {
-	private final PGPDigestCalculatorProvider digestProvider;
-
-	/**
-	 * Base constructor.
-	 *
-	 * @param digestProvider
-	 *            a provider for digest calculations. Used to confirm key
-	 *            protection hashes.
-	 */
-	public SExprParser(PGPDigestCalculatorProvider digestProvider) {
-		this.digestProvider = digestProvider;
-	}
-
-	/**
-	 * Parse a secret key from one of the GPG S expression keys associating it
-	 * with the passed in public key.
-	 *
-	 * @param inputStream
-	 *            to read from
-	 * @param keyProtectionRemoverFactory
-	 *            for decrypting encrypted keys
-	 * @param pubKey
-	 *            the private key should belong to
-	 *
-	 * @return a secret key object.
-	 * @throws IOException
-	 *             if an IO error occurred
-	 * @throws PGPException
-	 *             if some PGP error occurred
-	 */
-	public PGPSecretKey parseSecretKey(InputStream inputStream,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory,
-			PGPPublicKey pubKey) throws IOException, PGPException {
-		SXprUtils.skipOpenParenthesis(inputStream);
-
-		String type;
-
-		type = SXprUtils.readString(inputStream, inputStream.read());
-		if (type.equals("protected-private-key")
-				|| type.equals("private-key")) {
-			SXprUtils.skipOpenParenthesis(inputStream);
-
-			String keyType = SXprUtils.readString(inputStream,
-					inputStream.read());
-			if (keyType.equals("ecc")) {
-				SXprUtils.skipOpenParenthesis(inputStream);
-
-				String curveID = SXprUtils.readString(inputStream,
-						inputStream.read());
-				String curveName = SXprUtils.readString(inputStream,
-						inputStream.read());
-
-				SXprUtils.skipCloseParenthesis(inputStream);
-
-				byte[] qVal;
-
-				SXprUtils.skipOpenParenthesis(inputStream);
-
-				type = SXprUtils.readString(inputStream, inputStream.read());
-				// JGit: c.f. https://github.com/bcgit/bc-java/issues/1590.
-				// There may be a flags sub-list here for ed25519 or curve25519.
-				if (type.equals("flags")) {
-					SXprUtils.readString(inputStream, inputStream.read());
-					SXprUtils.skipCloseParenthesis(inputStream);
-					SXprUtils.skipOpenParenthesis(inputStream);
-					type = SXprUtils.readString(inputStream,
-							inputStream.read());
-				}
-				if (type.equals("q")) {
-					qVal = SXprUtils.readBytes(inputStream, inputStream.read());
-				} else {
-					throw new PGPException("no q value found");
-				}
-
-				SXprUtils.skipCloseParenthesis(inputStream);
-
-				BigInteger d = processECSecretKey(inputStream, curveID,
-						curveName, qVal, keyProtectionRemoverFactory);
-
-				if (curveName.startsWith("NIST ")) {
-					curveName = curveName.substring("NIST ".length());
-				}
-
-				// JGit: BC doesn't know Ed25519 curve name.
-				ASN1ObjectIdentifier curveOid = ECNamedCurveTable
-						.getOID(curveName);
-				if (curveOid == null) {
-					curveOid = ObjectIds.getByName(curveName);
-				}
-				ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(
-						curveOid,
-						new BigInteger(1, qVal));
-				ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey) pubKey
-						.getPublicKeyPacket().getKey();
-				if (!ObjectIds.match(basePubKey.getCurveOID(),
-						assocPubKey.getCurveOID())
-						|| !basePubKey.getEncodedPoint()
-								.equals(assocPubKey.getEncodedPoint())) {
-					throw new PGPException(
-							"passed in public key does not match secret key");
-				}
-
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubKey.getPublicKeyPacket(),
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new ECSecretBCPGKey(d).getEncoded()),
-						pubKey);
-			} else if (keyType.equals("dsa")) {
-				BigInteger p = readBigInteger("p", inputStream);
-				BigInteger q = readBigInteger("q", inputStream);
-				BigInteger g = readBigInteger("g", inputStream);
-
-				BigInteger y = readBigInteger("y", inputStream);
-
-				BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
-						keyProtectionRemoverFactory);
-
-				DSAPublicBCPGKey basePubKey = new DSAPublicBCPGKey(p, q, g, y);
-				DSAPublicBCPGKey assocPubKey = (DSAPublicBCPGKey) pubKey
-						.getPublicKeyPacket().getKey();
-				if (!basePubKey.getP().equals(assocPubKey.getP())
-						|| !basePubKey.getQ().equals(assocPubKey.getQ())
-						|| !basePubKey.getG().equals(assocPubKey.getG())
-						|| !basePubKey.getY().equals(assocPubKey.getY())) {
-					throw new PGPException(
-							"passed in public key does not match secret key");
-				}
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubKey.getPublicKeyPacket(),
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new DSASecretBCPGKey(x).getEncoded()),
-						pubKey);
-			} else if (keyType.equals("elg")) {
-				BigInteger p = readBigInteger("p", inputStream);
-				BigInteger g = readBigInteger("g", inputStream);
-
-				BigInteger y = readBigInteger("y", inputStream);
-
-				BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
-						keyProtectionRemoverFactory);
-
-				ElGamalPublicBCPGKey basePubKey = new ElGamalPublicBCPGKey(p, g,
-						y);
-				ElGamalPublicBCPGKey assocPubKey = (ElGamalPublicBCPGKey) pubKey
-						.getPublicKeyPacket().getKey();
-				if (!basePubKey.getP().equals(assocPubKey.getP())
-						|| !basePubKey.getG().equals(assocPubKey.getG())
-						|| !basePubKey.getY().equals(assocPubKey.getY())) {
-					throw new PGPException(
-							"passed in public key does not match secret key");
-				}
-
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubKey.getPublicKeyPacket(),
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new ElGamalSecretBCPGKey(x).getEncoded()),
-						pubKey);
-			} else if (keyType.equals("rsa")) {
-				BigInteger n = readBigInteger("n", inputStream);
-				BigInteger e = readBigInteger("e", inputStream);
-
-				BigInteger[] values = processRSASecretKey(inputStream, n, e,
-						keyProtectionRemoverFactory);
-
-				// TODO: type of RSA key?
-				RSAPublicBCPGKey basePubKey = new RSAPublicBCPGKey(n, e);
-				RSAPublicBCPGKey assocPubKey = (RSAPublicBCPGKey) pubKey
-						.getPublicKeyPacket().getKey();
-				if (!basePubKey.getModulus().equals(assocPubKey.getModulus())
-						|| !basePubKey.getPublicExponent()
-								.equals(assocPubKey.getPublicExponent())) {
-					throw new PGPException(
-							"passed in public key does not match secret key");
-				}
-
-				return new PGPSecretKey(new SecretKeyPacket(
-						pubKey.getPublicKeyPacket(),
-						SymmetricKeyAlgorithmTags.NULL, null, null,
-						new RSASecretBCPGKey(values[0], values[1], values[2])
-								.getEncoded()),
-						pubKey);
-			} else {
-				throw new PGPException("unknown key type: " + keyType);
-			}
-		}
-
-		throw new PGPException("unknown key type found");
-	}
-
-	/**
-	 * Parse a secret key from one of the GPG S expression keys.
-	 *
-	 * @param inputStream
-	 *            to read from
-	 * @param keyProtectionRemoverFactory
-	 *            for decrypting encrypted keys
-	 * @param fingerPrintCalculator
-	 *            for calculating key fingerprints
-	 *
-	 * @return a secret key object.
-	 * @throws IOException
-	 *             if an IO error occurred
-	 * @throws PGPException
-	 *             if a PGP error occurred
-	 */
-	public PGPSecretKey parseSecretKey(InputStream inputStream,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory,
-			KeyFingerPrintCalculator fingerPrintCalculator)
-			throws IOException, PGPException {
-		SXprUtils.skipOpenParenthesis(inputStream);
-
-		String type;
-
-		type = SXprUtils.readString(inputStream, inputStream.read());
-		if (type.equals("protected-private-key")
-				|| type.equals("private-key")) {
-			SXprUtils.skipOpenParenthesis(inputStream);
-
-			String keyType = SXprUtils.readString(inputStream,
-					inputStream.read());
-			if (keyType.equals("ecc")) {
-				SXprUtils.skipOpenParenthesis(inputStream);
-
-				String curveID = SXprUtils.readString(inputStream,
-						inputStream.read());
-				String curveName = SXprUtils.readString(inputStream,
-						inputStream.read());
-
-				if (curveName.startsWith("NIST ")) {
-					curveName = curveName.substring("NIST ".length());
-				}
-
-				SXprUtils.skipCloseParenthesis(inputStream);
-
-				byte[] qVal;
-
-				SXprUtils.skipOpenParenthesis(inputStream);
-
-				type = SXprUtils.readString(inputStream, inputStream.read());
-				// JGit: c.f. https://github.com/bcgit/bc-java/issues/1590.
-				// There may be a flags sub-list here for ed25519 or curve25519.
-				if (type.equals("flags")) {
-					SXprUtils.readString(inputStream, inputStream.read());
-					SXprUtils.skipCloseParenthesis(inputStream);
-					SXprUtils.skipOpenParenthesis(inputStream);
-					type = SXprUtils.readString(inputStream,
-							inputStream.read());
-				}
-				if (type.equals("q")) {
-					qVal = SXprUtils.readBytes(inputStream, inputStream.read());
-				} else {
-					throw new PGPException("no q value found");
-				}
-
-				PublicKeyPacket pubPacket = new PublicKeyPacket(
-						PublicKeyAlgorithmTags.ECDSA, new Date(),
-						new ECDSAPublicBCPGKey(
-								ECNamedCurveTable.getOID(curveName),
-								new BigInteger(1, qVal)));
-
-				SXprUtils.skipCloseParenthesis(inputStream);
-
-				BigInteger d = processECSecretKey(inputStream, curveID,
-						curveName, qVal, keyProtectionRemoverFactory);
-
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubPacket,
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new ECSecretBCPGKey(d).getEncoded()),
-						new PGPPublicKey(pubPacket, fingerPrintCalculator));
-			} else if (keyType.equals("dsa")) {
-				BigInteger p = readBigInteger("p", inputStream);
-				BigInteger q = readBigInteger("q", inputStream);
-				BigInteger g = readBigInteger("g", inputStream);
-
-				BigInteger y = readBigInteger("y", inputStream);
-
-				BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
-						keyProtectionRemoverFactory);
-
-				PublicKeyPacket pubPacket = new PublicKeyPacket(
-						PublicKeyAlgorithmTags.DSA, new Date(),
-						new DSAPublicBCPGKey(p, q, g, y));
-
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubPacket,
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new DSASecretBCPGKey(x).getEncoded()),
-						new PGPPublicKey(pubPacket, fingerPrintCalculator));
-			} else if (keyType.equals("elg")) {
-				BigInteger p = readBigInteger("p", inputStream);
-				BigInteger g = readBigInteger("g", inputStream);
-
-				BigInteger y = readBigInteger("y", inputStream);
-
-				BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
-						keyProtectionRemoverFactory);
-
-				PublicKeyPacket pubPacket = new PublicKeyPacket(
-						PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, new Date(),
-						new ElGamalPublicBCPGKey(p, g, y));
-
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubPacket,
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new ElGamalSecretBCPGKey(x).getEncoded()),
-						new PGPPublicKey(pubPacket, fingerPrintCalculator));
-			} else if (keyType.equals("rsa")) {
-				BigInteger n = readBigInteger("n", inputStream);
-				BigInteger e = readBigInteger("e", inputStream);
-
-				BigInteger[] values = processRSASecretKey(inputStream, n, e,
-						keyProtectionRemoverFactory);
-
-				// TODO: type of RSA key?
-				PublicKeyPacket pubPacket = new PublicKeyPacket(
-						PublicKeyAlgorithmTags.RSA_GENERAL, new Date(),
-						new RSAPublicBCPGKey(n, e));
-
-				return new PGPSecretKey(
-						new SecretKeyPacket(pubPacket,
-								SymmetricKeyAlgorithmTags.NULL, null, null,
-								new RSASecretBCPGKey(values[0], values[1],
-										values[2]).getEncoded()),
-						new PGPPublicKey(pubPacket, fingerPrintCalculator));
-			} else {
-				throw new PGPException("unknown key type: " + keyType);
-			}
-		}
-
-		throw new PGPException("unknown key type found");
-	}
-
-	private BigInteger readBigInteger(String expectedType,
-			InputStream inputStream) throws IOException, PGPException {
-		SXprUtils.skipOpenParenthesis(inputStream);
-
-		String type = SXprUtils.readString(inputStream, inputStream.read());
-		if (!type.equals(expectedType)) {
-			throw new PGPException(expectedType + " value expected");
-		}
-
-		byte[] nBytes = SXprUtils.readBytes(inputStream, inputStream.read());
-		BigInteger v = new BigInteger(1, nBytes);
-
-		SXprUtils.skipCloseParenthesis(inputStream);
-
-		return v;
-	}
-
-	private static byte[][] extractData(InputStream inputStream,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
-			throws PGPException, IOException {
-		byte[] data;
-		byte[] protectedAt = null;
-
-		SXprUtils.skipOpenParenthesis(inputStream);
-
-		String type = SXprUtils.readString(inputStream, inputStream.read());
-		if (type.equals("protected")) {
-			String protection = SXprUtils.readString(inputStream,
-					inputStream.read());
-
-			SXprUtils.skipOpenParenthesis(inputStream);
-
-			S2K s2k = SXprUtils.parseS2K(inputStream);
-
-			byte[] iv = SXprUtils.readBytes(inputStream, inputStream.read());
-
-			SXprUtils.skipCloseParenthesis(inputStream);
-
-			byte[] secKeyData = SXprUtils.readBytes(inputStream,
-					inputStream.read());
-
-			SXprUtils.skipCloseParenthesis(inputStream);
-
-			PBESecretKeyDecryptor keyDecryptor = keyProtectionRemoverFactory
-					.createDecryptor(protection);
-
-			// TODO: recognise other algorithms
-			byte[] key = keyDecryptor.makeKeyFromPassPhrase(
-					SymmetricKeyAlgorithmTags.AES_128, s2k);
-
-			data = keyDecryptor.recoverKeyData(
-					SymmetricKeyAlgorithmTags.AES_128, key, iv, secKeyData, 0,
-					secKeyData.length);
-
-			// check if protected at is present
-			if (inputStream.read() == '(') {
-				ByteArrayOutputStream bOut = new ByteArrayOutputStream();
-
-				bOut.write('(');
-				int ch;
-				while ((ch = inputStream.read()) >= 0 && ch != ')') {
-					bOut.write(ch);
-				}
-
-				if (ch != ')') {
-					throw new IOException("unexpected end to SExpr");
-				}
-
-				bOut.write(')');
-
-				protectedAt = bOut.toByteArray();
-			}
-
-			SXprUtils.skipCloseParenthesis(inputStream);
-			SXprUtils.skipCloseParenthesis(inputStream);
-		} else if (type.equals("d") || type.equals("x")) {
-			// JGit modification: unencrypted DSA or ECC keys can have an "x"
-			// here
-			return null;
-		} else {
-			throw new PGPException("protected block not found");
-		}
-
-		return new byte[][] { data, protectedAt };
-	}
-
-	private BigInteger processDSASecretKey(InputStream inputStream,
-			BigInteger p, BigInteger q, BigInteger g, BigInteger y,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
-			throws IOException, PGPException {
-		String type;
-		byte[][] basicData = extractData(inputStream,
-				keyProtectionRemoverFactory);
-
-		// JGit modification: handle unencrypted DSA keys
-		if (basicData == null) {
-			byte[] nBytes = SXprUtils.readBytes(inputStream,
-					inputStream.read());
-			BigInteger x = new BigInteger(1, nBytes);
-			SXprUtils.skipCloseParenthesis(inputStream);
-			return x;
-		}
-
-		byte[] keyData = basicData[0];
-		byte[] protectedAt = basicData[1];
-
-		//
-		// parse the secret key S-expr
-		//
-		InputStream keyIn = new ByteArrayInputStream(keyData);
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-		SXprUtils.skipOpenParenthesis(keyIn);
-
-		BigInteger x = readBigInteger("x", keyIn);
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		// JGit modification: OCB-encrypted keys don't have and don't need a
-		// hash
-		if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
-			return x;
-		}
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("hash")) {
-			throw new PGPException("hash keyword expected");
-		}
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("sha1")) {
-			throw new PGPException("hash keyword expected");
-		}
-
-		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		if (digestProvider != null) {
-			PGPDigestCalculator digestCalculator = digestProvider
-					.get(HashAlgorithmTags.SHA1);
-
-			OutputStream dOut = digestCalculator.getOutputStream();
-
-			dOut.write(Strings.toByteArray("(3:dsa"));
-			writeCanonical(dOut, "p", p);
-			writeCanonical(dOut, "q", q);
-			writeCanonical(dOut, "g", g);
-			writeCanonical(dOut, "y", y);
-			writeCanonical(dOut, "x", x);
-
-			// check protected-at
-			if (protectedAt != null) {
-				dOut.write(protectedAt);
-			}
-
-			dOut.write(Strings.toByteArray(")"));
-
-			byte[] check = digestCalculator.getDigest();
-			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
-				throw new PGPException(
-						"checksum on protected data failed in SExpr");
-			}
-		}
-
-		return x;
-	}
-
-	private BigInteger processElGamalSecretKey(InputStream inputStream,
-			BigInteger p, BigInteger g, BigInteger y,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
-			throws IOException, PGPException {
-		String type;
-		byte[][] basicData = extractData(inputStream,
-				keyProtectionRemoverFactory);
-
-		// JGit modification: handle unencrypted EC keys
-		if (basicData == null) {
-			byte[] nBytes = SXprUtils.readBytes(inputStream,
-					inputStream.read());
-			BigInteger x = new BigInteger(1, nBytes);
-			SXprUtils.skipCloseParenthesis(inputStream);
-			return x;
-		}
-
-		byte[] keyData = basicData[0];
-		byte[] protectedAt = basicData[1];
-
-		//
-		// parse the secret key S-expr
-		//
-		InputStream keyIn = new ByteArrayInputStream(keyData);
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-		SXprUtils.skipOpenParenthesis(keyIn);
-
-		BigInteger x = readBigInteger("x", keyIn);
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		// JGit modification: OCB-encrypted keys don't have and don't need a
-		// hash
-		if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
-			return x;
-		}
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("hash")) {
-			throw new PGPException("hash keyword expected");
-		}
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("sha1")) {
-			throw new PGPException("hash keyword expected");
-		}
-
-		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		if (digestProvider != null) {
-			PGPDigestCalculator digestCalculator = digestProvider
-					.get(HashAlgorithmTags.SHA1);
-
-			OutputStream dOut = digestCalculator.getOutputStream();
-
-			dOut.write(Strings.toByteArray("(3:elg"));
-			writeCanonical(dOut, "p", p);
-			writeCanonical(dOut, "g", g);
-			writeCanonical(dOut, "y", y);
-			writeCanonical(dOut, "x", x);
-
-			// check protected-at
-			if (protectedAt != null) {
-				dOut.write(protectedAt);
-			}
-
-			dOut.write(Strings.toByteArray(")"));
-
-			byte[] check = digestCalculator.getDigest();
-			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
-				throw new PGPException(
-						"checksum on protected data failed in SExpr");
-			}
-		}
-
-		return x;
-	}
-
-	private BigInteger processECSecretKey(InputStream inputStream,
-			String curveID, String curveName, byte[] qVal,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
-			throws IOException, PGPException {
-		String type;
-
-		byte[][] basicData = extractData(inputStream,
-				keyProtectionRemoverFactory);
-
-		// JGit modification: handle unencrypted EC keys
-		if (basicData == null) {
-			byte[] nBytes = SXprUtils.readBytes(inputStream,
-					inputStream.read());
-			BigInteger d = new BigInteger(1, nBytes);
-			SXprUtils.skipCloseParenthesis(inputStream);
-			return d;
-		}
-
-		byte[] keyData = basicData[0];
-		byte[] protectedAt = basicData[1];
-
-		//
-		// parse the secret key S-expr
-		//
-		InputStream keyIn = new ByteArrayInputStream(keyData);
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-		SXprUtils.skipOpenParenthesis(keyIn);
-		BigInteger d = readBigInteger("d", keyIn);
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		// JGit modification: OCB-encrypted keys don't have and don't need a
-		// hash
-		if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
-			return d;
-		}
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("hash")) {
-			throw new PGPException("hash keyword expected");
-		}
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("sha1")) {
-			throw new PGPException("hash keyword expected");
-		}
-
-		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		if (digestProvider != null) {
-			PGPDigestCalculator digestCalculator = digestProvider
-					.get(HashAlgorithmTags.SHA1);
-
-			OutputStream dOut = digestCalculator.getOutputStream();
-
-			dOut.write(Strings.toByteArray("(3:ecc"));
-
-			dOut.write(Strings.toByteArray("(" + curveID.length() + ":"
-					+ curveID + curveName.length() + ":" + curveName + ")"));
-
-			writeCanonical(dOut, "q", qVal);
-			writeCanonical(dOut, "d", d);
-
-			// check protected-at
-			if (protectedAt != null) {
-				dOut.write(protectedAt);
-			}
-
-			dOut.write(Strings.toByteArray(")"));
-
-			byte[] check = digestCalculator.getDigest();
-
-			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
-				throw new PGPException(
-						"checksum on protected data failed in SExpr");
-			}
-		}
-
-		return d;
-	}
-
-	private BigInteger[] processRSASecretKey(InputStream inputStream,
-			BigInteger n, BigInteger e,
-			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
-			throws IOException, PGPException {
-		String type;
-		byte[][] basicData = extractData(inputStream,
-				keyProtectionRemoverFactory);
-
-		byte[] keyData;
-		byte[] protectedAt = null;
-
-		InputStream keyIn;
-		BigInteger d;
-
-		if (basicData == null) {
-			keyIn = inputStream;
-			byte[] nBytes = SXprUtils.readBytes(inputStream,
-					inputStream.read());
-			d = new BigInteger(1, nBytes);
-
-			SXprUtils.skipCloseParenthesis(inputStream);
-
-		} else {
-			keyData = basicData[0];
-			protectedAt = basicData[1];
-
-			keyIn = new ByteArrayInputStream(keyData);
-
-			SXprUtils.skipOpenParenthesis(keyIn);
-			SXprUtils.skipOpenParenthesis(keyIn);
-			d = readBigInteger("d", keyIn);
-		}
-
-		//
-		// parse the secret key S-expr
-		//
-
-		BigInteger p = readBigInteger("p", keyIn);
-		BigInteger q = readBigInteger("q", keyIn);
-		BigInteger u = readBigInteger("u", keyIn);
-
-		// JGit modification: OCB-encrypted keys don't have and don't need a
-		// hash
-		if (basicData == null
-				|| keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
-			return new BigInteger[] { d, p, q, u };
-		}
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		SXprUtils.skipOpenParenthesis(keyIn);
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("hash")) {
-			throw new PGPException("hash keyword expected");
-		}
-		type = SXprUtils.readString(keyIn, keyIn.read());
-
-		if (!type.equals("sha1")) {
-			throw new PGPException("hash keyword expected");
-		}
-
-		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
-
-		SXprUtils.skipCloseParenthesis(keyIn);
-
-		if (digestProvider != null) {
-			PGPDigestCalculator digestCalculator = digestProvider
-					.get(HashAlgorithmTags.SHA1);
-
-			OutputStream dOut = digestCalculator.getOutputStream();
-
-			dOut.write(Strings.toByteArray("(3:rsa"));
-
-			writeCanonical(dOut, "n", n);
-			writeCanonical(dOut, "e", e);
-			writeCanonical(dOut, "d", d);
-			writeCanonical(dOut, "p", p);
-			writeCanonical(dOut, "q", q);
-			writeCanonical(dOut, "u", u);
-
-			// check protected-at
-			if (protectedAt != null) {
-				dOut.write(protectedAt);
-			}
-
-			dOut.write(Strings.toByteArray(")"));
-
-			byte[] check = digestCalculator.getDigest();
-
-			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
-				throw new PGPException(
-						"checksum on protected data failed in SExpr");
-			}
-		}
-
-		return new BigInteger[] { d, p, q, u };
-	}
-
-	private void writeCanonical(OutputStream dOut, String label, BigInteger i)
-			throws IOException {
-		writeCanonical(dOut, label, i.toByteArray());
-	}
-
-	private void writeCanonical(OutputStream dOut, String label, byte[] data)
-			throws IOException {
-		dOut.write(Strings.toByteArray(
-				"(" + label.length() + ":" + label + data.length + ":"));
-		dOut.write(data);
-		dOut.write(Strings.toByteArray(")"));
-	}
-}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
deleted file mode 100644
index 220aa28..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
- * <p>
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
- * and associated documentation files (the "Software"), to deal in the Software without restriction,
- *including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- * </p>
- * <p>
- * The above copyright notice and this permission notice shall be included in all copies or substantial
- * portions of the Software.
- * </p>
- * <p>
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
- * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
- * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- * </p>
- */
-package org.eclipse.jgit.gpg.bc.internal.keys;
-
-// This class is an unmodified copy from Bouncy Castle; needed because it's package-visible only and used by SExprParser.
-
-import java.io.IOException;
-import java.io.InputStream;
-
-import org.bouncycastle.bcpg.HashAlgorithmTags;
-import org.bouncycastle.bcpg.S2K;
-import org.bouncycastle.util.io.Streams;
-
-/**
- * Utility functions for looking a S-expression keys. This class will move when
- * it finds a better home!
- * <p>
- * Format documented here:
- * http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/keyformat.txt;h=42c4b1f06faf1bbe71ffadc2fee0fad6bec91a97;hb=refs/heads/master
- * </p>
- */
-class SXprUtils {
-	private static int readLength(InputStream in, int ch) throws IOException {
-		int len = ch - '0';
-
-		while ((ch = in.read()) >= 0 && ch != ':') {
-			len = len * 10 + ch - '0';
-		}
-
-		return len;
-	}
-
-	static String readString(InputStream in, int ch) throws IOException {
-		int len = readLength(in, ch);
-
-		char[] chars = new char[len];
-
-		for (int i = 0; i != chars.length; i++) {
-			chars[i] = (char) in.read();
-		}
-
-		return new String(chars);
-	}
-
-	static byte[] readBytes(InputStream in, int ch) throws IOException {
-		int len = readLength(in, ch);
-
-		byte[] data = new byte[len];
-
-		Streams.readFully(in, data);
-
-		return data;
-	}
-
-	static S2K parseS2K(InputStream in) throws IOException {
-		skipOpenParenthesis(in);
-
-		// Algorithm is hard-coded to SHA1 below anyway.
-		readString(in, in.read());
-		byte[] iv = readBytes(in, in.read());
-		final long iterationCount = Long.parseLong(readString(in, in.read()));
-
-		skipCloseParenthesis(in);
-
-		// we have to return the actual iteration count provided.
-		S2K s2k = new S2K(HashAlgorithmTags.SHA1, iv, (int) iterationCount) {
-			@Override
-			public long getIterationCount() {
-				return iterationCount;
-			}
-		};
-
-		return s2k;
-	}
-
-	static void skipOpenParenthesis(InputStream in) throws IOException {
-		int ch = in.read();
-		if (ch != '(') {
-			throw new IOException(
-					"unknown character encountered: " + (char) ch); //$NON-NLS-1$
-		}
-	}
-
-	static void skipCloseParenthesis(InputStream in) throws IOException {
-		int ch = in.read();
-		if (ch != ')') {
-			throw new IOException("unknown character encountered"); //$NON-NLS-1$
-		}
-	}
-}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
index a659d38..a56e418 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
@@ -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,34 +9,36 @@
  */
 package org.eclipse.jgit.gpg.bc.internal.keys;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.StreamCorruptedException;
 import java.net.URISyntaxException;
-import java.nio.charset.StandardCharsets;
 import java.text.MessageFormat;
-import java.util.Arrays;
 
+import org.bouncycastle.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.gpg.PGPSecretKeyParser;
+import org.bouncycastle.gpg.SExprParser;
+import org.bouncycastle.openpgp.OpenedPGPKeyData;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPSecretKey;
 import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
 import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
 import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
-import org.bouncycastle.util.io.Streams;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
 import org.eclipse.jgit.gpg.bc.internal.BCText;
-import org.eclipse.jgit.util.RawParseUtils;
 
 /**
  * Utilities for reading GPG secret keys from a gpg-agent key file.
  */
 public final class SecretKeys {
 
+	// Maximum nesting depth of sub-lists in an S-Expression for a secret key.
+	private static final int MAX_SEXPR_NESTING = 20;
+
 	private SecretKeys() {
 		// No instantiation.
 	}
@@ -64,12 +66,6 @@ public interface PassphraseSupplier {
 				UnsupportedCredentialItem, URISyntaxException;
 	}
 
-	private static final byte[] PROTECTED_KEY = "protected-private-key" //$NON-NLS-1$
-			.getBytes(StandardCharsets.US_ASCII);
-
-	private static final byte[] OCB_PROTECTED = "openpgp-s2k3-ocb-aes" //$NON-NLS-1$
-			.getBytes(StandardCharsets.US_ASCII);
-
 	/**
 	 * Reads a GPG secret key from the given stream.
 	 *
@@ -99,500 +95,59 @@ public static PGPSecretKey readSecretKey(InputStream in,
 			PassphraseSupplier passphraseSupplier, PGPPublicKey publicKey)
 			throws IOException, PGPException, CanceledException,
 			UnsupportedCredentialItem, URISyntaxException {
-		byte[] data = Streams.readAll(in);
-		if (data.length == 0) {
-			throw new EOFException();
-		} else if (data.length < 4 + PROTECTED_KEY.length) {
-			// +4 for "(21:" for a binary protected key
-			throw new IOException(
-					MessageFormat.format(BCText.get().secretKeyTooShort,
-							Integer.toUnsignedString(data.length)));
+		OpenedPGPKeyData data;
+		try (InputStream keyIn = new BufferedInputStream(in)) {
+			data = PGPSecretKeyParser.parse(keyIn, MAX_SEXPR_NESTING);
 		}
-		SExprParser parser = new SExprParser(calculatorProvider);
-		byte firstChar = data[0];
-		try {
-			if (firstChar == '(') {
-				// Binary format.
-				PBEProtectionRemoverFactory decryptor = null;
-				if (matches(data, 4, PROTECTED_KEY)) {
-					// AES/CBC encrypted.
-					decryptor = new JcePBEProtectionRemoverFactory(
-							passphraseSupplier.getPassphrase(),
-							calculatorProvider);
-				}
-				try (InputStream sIn = new ByteArrayInputStream(data)) {
-					return parser.parseSecretKey(sIn, decryptor, publicKey);
-				}
-			}
-			// Assume it's the new key-value format.
-			try (ByteArrayInputStream keyIn = new ByteArrayInputStream(data)) {
-				byte[] rawData = keyFromNameValueFormat(keyIn);
-				if (!matches(rawData, 1, PROTECTED_KEY)) {
-					// Not encrypted human-readable format.
-					try (InputStream sIn = new ByteArrayInputStream(
-							convertSexpression(rawData))) {
-						return parser.parseSecretKey(sIn, null, publicKey);
-					}
-				}
-				// An encrypted key from a key-value file. Most likely AES/OCB
-				// encrypted.
-				boolean isOCB[] = { false };
-				byte[] sExp = convertSexpression(rawData, isOCB);
-				PBEProtectionRemoverFactory decryptor;
-				if (isOCB[0]) {
-					decryptor = new OCBPBEProtectionRemoverFactory(
-							passphraseSupplier.getPassphrase(),
-							calculatorProvider, getAad(sExp));
-				} else {
-					decryptor = new JcePBEProtectionRemoverFactory(
-							passphraseSupplier.getPassphrase(),
-							calculatorProvider);
-				}
-				try (InputStream sIn = new ByteArrayInputStream(sExp)) {
-					return parser.parseSecretKey(sIn, decryptor, publicKey);
-				}
-			}
-		} catch (IOException e) {
-			throw new PGPException(e.getLocalizedMessage(), e);
+		PBEProtectionRemoverFactory decryptor = null;
+		if (isProtected(data)) {
+			decryptor = new JcePBEProtectionRemoverFactory(
+					passphraseSupplier.getPassphrase(), calculatorProvider);
 		}
-	}
-
-	/**
-	 * Extract the AAD for the OCB decryption from an s-expression.
-	 *
-	 * @param sExp
-	 *            buffer containing a valid binary s-expression
-	 * @return the AAD
-	 */
-	private static byte[] getAad(byte[] sExp) {
-		// Given a key
-		// @formatter:off
-		// (protected-private-key (rsa ... (protected openpgp-s2k3-ocb-aes ... )(protected-at ...)))
-		//                        A        B                                    C                  D
-		// The AAD is [A..B)[C..D). (From the binary serialized form.)
-		// @formatter:on
-		int i = 1; // Skip initial '('
-		while (sExp[i] != '(') {
-			i++;
-		}
-		int aadStart = i++;
-		int aadEnd = skip(sExp, aadStart);
-		byte[] protectedPrefix = "(9:protected" //$NON-NLS-1$
-				.getBytes(StandardCharsets.US_ASCII);
-		while (!matches(sExp, i, protectedPrefix)) {
-			i++;
-		}
-		int protectedStart = i;
-		int protectedEnd = skip(sExp, protectedStart);
-		byte[] aadData = new byte[aadEnd - aadStart
-				- (protectedEnd - protectedStart)];
-		System.arraycopy(sExp, aadStart, aadData, 0, protectedStart - aadStart);
-		System.arraycopy(sExp, protectedEnd, aadData, protectedStart - aadStart,
-				aadEnd - protectedEnd);
-		return aadData;
-	}
-
-	/**
-	 * Skips a list including nested lists.
-	 *
-	 * @param sExp
-	 *            buffer containing valid binary s-expression data
-	 * @param start
-	 *            index of the opening '(' of the list to skip
-	 * @return the index after the closing ')' of the skipped list
-	 */
-	private static int skip(byte[] sExp, int start) {
-		int i = start + 1;
-		int depth = 1;
-		while (depth > 0) {
-			switch (sExp[i]) {
-			case '(':
-				depth++;
-				break;
-			case ')':
-				depth--;
-				break;
-			default:
-				// We must be on a length
-				int j = i;
-				while (sExp[j] >= '0' && sExp[j] <= '9') {
-					j++;
-				}
-				// j is on the colon
-				int length = Integer.parseInt(
-						new String(sExp, i, j - i, StandardCharsets.US_ASCII));
-				i = j + length;
+		switch (publicKey.getAlgorithm()) {
+		case PublicKeyAlgorithmTags.EDDSA_LEGACY:
+		case PublicKeyAlgorithmTags.Ed25519:
+			// If we let Bouncy Castle check whether the secret key matches the
+			// given public key it may get into trouble in some cases with
+			// ed25519 keys. It appears that we may end up with secret keys
+			// using the official RFC 8410 OID for ed25519, "1.3.101.112", while
+			// the public key passed in may have a non-standard OpenPGP-specific
+			// OID "1.3.6.1.4.1.11591.15.1", or vice versa. Bouncy Castle then
+			// throws an exception because of the different OIDs.
+			//
+			// The work-around is to just read the secret key, and double-check
+			// later that the OIDs are compatible and the curve points match.
+			PGPSecretKey secret = data.getKeyData(null, calculatorProvider,
+					decryptor, new JcaKeyFingerprintCalculator(),
+					MAX_SEXPR_NESTING);
+			PGPPublicKey pubKeyRead = secret.getPublicKey();
+			int algoRead = pubKeyRead.getAlgorithm();
+			if (algoRead != PublicKeyAlgorithmTags.EDDSA_LEGACY
+					&& algoRead != PublicKeyAlgorithmTags.Ed25519) {
+				throw new PGPException(BCText.get().keyAlgorithmMismatch);
 			}
-			i++;
-		}
-		return i;
-	}
-
-	/**
-	 * Checks whether the {@code needle} matches {@code src} at offset
-	 * {@code from}.
-	 *
-	 * @param src
-	 *            to match against {@code needle}
-	 * @param from
-	 *            position in {@code src} to start matching
-	 * @param needle
-	 *            to match against
-	 * @return {@code true} if {@code src} contains {@code needle} at position
-	 *         {@code from}, {@code false} otherwise
-	 */
-	private static boolean matches(byte[] src, int from, byte[] needle) {
-		if (from < 0 || from + needle.length > src.length) {
-			return false;
-		}
-		return org.bouncycastle.util.Arrays.constantTimeAreEqual(needle.length,
-				src, from, needle, 0);
-	}
-
-	/**
-	 * Converts a human-readable serialized s-expression into a binary
-	 * serialized s-expression.
-	 *
-	 * @param humanForm
-	 *            to convert
-	 * @return the converted s-expression
-	 * @throws IOException
-	 *             if the conversion fails
-	 */
-	private static byte[] convertSexpression(byte[] humanForm)
-			throws IOException {
-		boolean[] isOCB = { false };
-		return convertSexpression(humanForm, isOCB);
-	}
-
-	/**
-	 * Converts a human-readable serialized s-expression into a binary
-	 * serialized s-expression.
-	 *
-	 * @param humanForm
-	 *            to convert
-	 * @param isOCB
-	 *            returns whether the s-expression specified AES/OCB encryption
-	 * @return the converted s-expression
-	 * @throws IOException
-	 *             if the conversion fails
-	 */
-	private static byte[] convertSexpression(byte[] humanForm, boolean[] isOCB)
-			throws IOException {
-		int pos = 0;
-		try (ByteArrayOutputStream out = new ByteArrayOutputStream(
-				humanForm.length)) {
-			while (pos < humanForm.length) {
-				byte b = humanForm[pos];
-				if (b == '(' || b == ')') {
-					out.write(b);
-					pos++;
-				} else if (isGpgSpace(b)) {
-					pos++;
-				} else if (b == '#') {
-					// Hex value follows up to the next #
-					int i = ++pos;
-					while (i < humanForm.length && isHex(humanForm[i])) {
-						i++;
-					}
-					if (i == pos || humanForm[i] != '#') {
-						throw new StreamCorruptedException(
-								BCText.get().sexprHexNotClosed);
-					}
-					if ((i - pos) % 2 != 0) {
-						throw new StreamCorruptedException(
-								BCText.get().sexprHexOdd);
-					}
-					int l = (i - pos) / 2;
-					out.write(Integer.toString(l)
-							.getBytes(StandardCharsets.US_ASCII));
-					out.write(':');
-					while (pos < i) {
-						int x = (nibble(humanForm[pos]) << 4)
-								| nibble(humanForm[pos + 1]);
-						pos += 2;
-						out.write(x);
-					}
-					pos = i + 1;
-				} else if (isTokenChar(b)) {
-					// Scan the token
-					int start = pos++;
-					while (pos < humanForm.length
-							&& isTokenChar(humanForm[pos])) {
-						pos++;
-					}
-					int l = pos - start;
-					if (pos - start == OCB_PROTECTED.length
-							&& matches(humanForm, start, OCB_PROTECTED)) {
-						isOCB[0] = true;
-					}
-					out.write(Integer.toString(l)
-							.getBytes(StandardCharsets.US_ASCII));
-					out.write(':');
-					out.write(humanForm, start, pos - start);
-				} else if (b == '"') {
-					// Potentially quoted string.
-					int start = ++pos;
-					boolean escaped = false;
-					while (pos < humanForm.length
-							&& (escaped || humanForm[pos] != '"')) {
-						int ch = humanForm[pos++];
-						escaped = !escaped && ch == '\\';
-					}
-					if (pos >= humanForm.length) {
-						throw new StreamCorruptedException(
-								BCText.get().sexprStringNotClosed);
-					}
-					// start is on the first character of the string, pos on the
-					// closing quote.
-					byte[] dq = dequote(humanForm, start, pos);
-					out.write(Integer.toString(dq.length)
-							.getBytes(StandardCharsets.US_ASCII));
-					out.write(':');
-					out.write(dq);
-					pos++;
-				} else {
-					throw new StreamCorruptedException(
-							MessageFormat.format(BCText.get().sexprUnhandled,
-									Integer.toHexString(b & 0xFF)));
-				}
+			ECPublicBCPGKey ec1 = (ECPublicBCPGKey) publicKey
+					.getPublicKeyPacket().getKey();
+			ECPublicBCPGKey ec2 = (ECPublicBCPGKey) pubKeyRead
+					.getPublicKeyPacket().getKey();
+			if (!ObjectIds.match(ec1.getCurveOID(), ec2.getCurveOID())
+					|| !ec1.getEncodedPoint().equals(ec2.getEncodedPoint())) {
+				throw new PGPException(
+						MessageFormat.format(BCText.get().keyMismatch,
+								ec1.getCurveOID(), ec1.getEncodedPoint(),
+								ec2.getCurveOID(), ec2.getEncodedPoint()));
 			}
-			return out.toByteArray();
-		}
-	}
-
-	/**
-	 * GPG-style string de-quoting, which is basically C-style, with some
-	 * literal CR/LF escaping.
-	 *
-	 * @param in
-	 *            buffer containing the quoted string
-	 * @param from
-	 *            index after the opening quote in {@code in}
-	 * @param to
-	 *            index of the closing quote in {@code in}
-	 * @return the dequoted raw string value
-	 * @throws StreamCorruptedException
-	 *             if object stream is corrupt
-	 */
-	private static byte[] dequote(byte[] in, int from, int to)
-			throws StreamCorruptedException {
-		// Result must be shorter or have the same length
-		byte[] out = new byte[to - from];
-		int j = 0;
-		int i = from;
-		while (i < to) {
-			byte b = in[i++];
-			if (b != '\\') {
-				out[j++] = b;
-				continue;
-			}
-			if (i == to) {
-				throw new StreamCorruptedException(
-						BCText.get().sexprStringInvalidEscapeAtEnd);
-			}
-			b = in[i++];
-			switch (b) {
-			case 'b':
-				out[j++] = '\b';
-				break;
-			case 'f':
-				out[j++] = '\f';
-				break;
-			case 'n':
-				out[j++] = '\n';
-				break;
-			case 'r':
-				out[j++] = '\r';
-				break;
-			case 't':
-				out[j++] = '\t';
-				break;
-			case 'v':
-				out[j++] = 0x0B;
-				break;
-			case '"':
-			case '\'':
-			case '\\':
-				out[j++] = b;
-				break;
-			case '\r':
-				// Escaped literal line end. If an LF is following, skip that,
-				// too.
-				if (i < to && in[i] == '\n') {
-					i++;
-				}
-				break;
-			case '\n':
-				// Same for LF possibly followed by CR.
-				if (i < to && in[i] == '\r') {
-					i++;
-				}
-				break;
-			case 'x':
-				if (i + 1 >= to || !isHex(in[i]) || !isHex(in[i + 1])) {
-					throw new StreamCorruptedException(
-							BCText.get().sexprStringInvalidHexEscape);
-				}
-				out[j++] = (byte) ((nibble(in[i]) << 4) | nibble(in[i + 1]));
-				i += 2;
-				break;
-			case '0':
-			case '1':
-			case '2':
-			case '3':
-				if (i + 2 >= to || !isOctal(in[i]) || !isOctal(in[i + 1])
-						|| !isOctal(in[i + 2])) {
-					throw new StreamCorruptedException(
-							BCText.get().sexprStringInvalidOctalEscape);
-				}
-				out[j++] = (byte) (((((in[i] - '0') << 3)
-						| (in[i + 1] - '0')) << 3) | (in[i + 2] - '0'));
-				i += 3;
-				break;
-			default:
-				throw new StreamCorruptedException(MessageFormat.format(
-						BCText.get().sexprStringInvalidEscape,
-						Integer.toHexString(b & 0xFF)));
-			}
-		}
-		return Arrays.copyOf(out, j);
-	}
-
-	/**
-	 * Extracts the key from a GPG name-value-pair key file.
-	 * <p>
-	 * Package-visible for tests only.
-	 * </p>
-	 *
-	 * @param in
-	 *            {@link InputStream} to read from; should be buffered
-	 * @return the raw key data as extracted from the file
-	 * @throws IOException
-	 *             if the {@code in} stream cannot be read or does not contain a
-	 *             key
-	 */
-	static byte[] keyFromNameValueFormat(InputStream in) throws IOException {
-		// It would be nice if we could use RawParseUtils here, but GPG compares
-		// names case-insensitively. We're only interested in the "Key:"
-		// name-value pair.
-		int[] nameLow = { 'k', 'e', 'y', ':' };
-		int[] nameCap = { 'K', 'E', 'Y', ':' };
-		int nameIdx = 0;
-		for (;;) {
-			int next = in.read();
-			if (next < 0) {
-				throw new EOFException();
-			}
-			if (next == '\n') {
-				nameIdx = 0;
-			} else if (nameIdx >= 0) {
-				if (nameLow[nameIdx] == next || nameCap[nameIdx] == next) {
-					nameIdx++;
-					if (nameIdx == nameLow.length) {
-						break;
-					}
-				} else {
-					nameIdx = -1;
-				}
-			}
-		}
-		// We're after "Key:". Read the value as continuation lines.
-		int last = ':';
-		byte[] rawData;
-		try (ByteArrayOutputStream out = new ByteArrayOutputStream(8192)) {
-			for (;;) {
-				int next = in.read();
-				if (next < 0) {
-					break;
-				}
-				if (last == '\n') {
-					if (next == ' ' || next == '\t') {
-						// Continuation line; skip this whitespace
-						last = next;
-						continue;
-					}
-					break; // Not a continuation line
-				}
-				out.write(next);
-				last = next;
-			}
-			rawData = out.toByteArray();
-		}
-		// GPG trims off trailing whitespace, and a line having only whitespace
-		// is a single LF.
-		try (ByteArrayOutputStream out = new ByteArrayOutputStream(
-				rawData.length)) {
-			int lineStart = 0;
-			boolean trimLeading = true;
-			while (lineStart < rawData.length) {
-				int nextLineStart = RawParseUtils.nextLF(rawData, lineStart);
-				if (trimLeading) {
-					while (lineStart < nextLineStart
-							&& isGpgSpace(rawData[lineStart])) {
-						lineStart++;
-					}
-				}
-				// Trim trailing
-				int i = nextLineStart - 1;
-				while (lineStart < i && isGpgSpace(rawData[i])) {
-					i--;
-				}
-				if (i <= lineStart) {
-					// Empty line signifies LF
-					out.write('\n');
-					trimLeading = true;
-				} else {
-					out.write(rawData, lineStart, i - lineStart + 1);
-					trimLeading = false;
-				}
-				lineStart = nextLineStart;
-			}
-			return out.toByteArray();
-		}
-	}
-
-	private static boolean isGpgSpace(int ch) {
-		return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
-	}
-
-	private static boolean isTokenChar(int ch) {
-		switch (ch) {
-		case '-':
-		case '.':
-		case '/':
-		case '_':
-		case ':':
-		case '*':
-		case '+':
-		case '=':
-			return true;
+			return secret;
 		default:
-			if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
-					|| (ch >= '0' && ch <= '9')) {
-				return true;
-			}
-			return false;
+			// For other key types let Bouncy Castle do the check.
+			return data.getKeyData(publicKey, calculatorProvider, decryptor,
+					null, MAX_SEXPR_NESTING);
 		}
 	}
 
-	private static boolean isHex(int ch) {
-		return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
-				|| (ch >= 'a' && ch <= 'f');
+	private static boolean isProtected(OpenedPGPKeyData data) {
+		return SExprParser.ProtectionFormatTypeTags.PROTECTED_PRIVATE_KEY == SExprParser
+				.getProtectionType(data.getKeyExpression().getString(0));
 	}
 
-	private static boolean isOctal(int ch) {
-		return (ch >= '0' && ch <= '7');
-	}
-
-	private static int nibble(int ch) {
-		if (ch >= '0' && ch <= '9') {
-			return ch - '0';
-		} else if (ch >= 'A' && ch <= 'F') {
-			return ch - 'A' + 10;
-		} else if (ch >= 'a' && ch <= 'f') {
-			return ch - 'a' + 10;
-		}
-		return -1;
-	}
 }
diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
index 18238b5..20d0e4d 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.apache
 Bundle-SymbolicName: org.eclipse.jgit.http.apache
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
@@ -26,11 +26,11 @@
  org.apache.http.impl.conn;version="[4.4.0,5.0.0)",
  org.apache.http.params;version="[4.3.0,5.0.0)",
  org.apache.http.ssl;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)"
-Export-Package: org.eclipse.jgit.transport.http.apache;version="7.0.2";
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)"
+Export-Package: org.eclipse.jgit.transport.http.apache;version="7.1.2";
   uses:="org.apache.http.client,
    org.eclipse.jgit.transport.http,
    org.apache.http.entity,
diff --git a/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF
index 89b7db1..04bbe37 100644
--- a/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.http.apache - Sources
 Bundle-SymbolicName: org.eclipse.jgit.http.apache.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.apache/pom.xml b/org.eclipse.jgit.http.apache/pom.xml
index ec29cbd..11d92fc 100644
--- a/org.eclipse.jgit.http.apache/pom.xml
+++ b/org.eclipse.jgit.http.apache/pom.xml
@@ -15,7 +15,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.apache</artifactId>
diff --git a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
index b0b1ae1..2a262d1 100644
--- a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
@@ -3,14 +3,14 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.server
 Bundle-SymbolicName: org.eclipse.jgit.http.server
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.http.server;version="7.0.2",
- org.eclipse.jgit.http.server.glue;version="7.0.2";
+Export-Package: org.eclipse.jgit.http.server;version="7.1.2",
+ org.eclipse.jgit.http.server.glue;version="7.1.2";
   uses:="jakarta.servlet,
   	jakarta.servlet.http",
- org.eclipse.jgit.http.server.resolver;version="7.0.2";
+ org.eclipse.jgit.http.server.resolver;version="7.1.2";
   uses:="jakarta.servlet.http
    org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.lib,
@@ -19,14 +19,14 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: jakarta.servlet;version="[6.0.0,7.0.0)",
  jakarta.servlet.http;version="[6.0.0,7.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.parser;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)"
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)"
diff --git a/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF
index 4239578..0d9322c 100644
--- a/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.http.server - Sources
 Bundle-SymbolicName: org.eclipse.jgit.http.server.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.server/pom.xml b/org.eclipse.jgit.http.server/pom.xml
index e13c39e..ecf80a3 100644
--- a/org.eclipse.jgit.http.server/pom.xml
+++ b/org.eclipse.jgit.http.server/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.server</artifactId>
diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
index b338ec4..ec2d986 100644
--- a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.test
 Bundle-SymbolicName: org.eclipse.jgit.http.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -29,26 +29,26 @@
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.security;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.thread;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.http.server;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.http.server.glue;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.http.server.resolver;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.http.server;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.http.server.glue;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.http.server.resolver;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http.apache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index c69fe90..13c9acb 100644
--- a/org.eclipse.jgit.http.test/pom.xml
+++ b/org.eclipse.jgit.http.test/pom.xml
@@ -18,7 +18,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.test</artifactId>
diff --git a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
index e4c2174..d60dbed 100644
--- a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit.http
 Bundle-SymbolicName: org.eclipse.jgit.junit.http
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
@@ -22,17 +22,17 @@
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.security;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.ssl;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.http.server;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.http.server;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.slf4j.helpers;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.junit.http;version="7.0.2";
+Export-Package: org.eclipse.jgit.junit.http;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    jakarta.servlet,
    jakarta.servlet.http,
diff --git a/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF
index b290a02..32c5433 100644
--- a/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.junit.http - Sources
 Bundle-SymbolicName: org.eclipse.jgit.junit.http.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.http/pom.xml b/org.eclipse.jgit.junit.http/pom.xml
index f853464..0ff0cd9 100644
--- a/org.eclipse.jgit.junit.http/pom.xml
+++ b/org.eclipse.jgit.junit.http/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.http</artifactId>
diff --git a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
index dfa1b7a..1f9dcf4 100644
--- a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit.ssh
 Bundle-SymbolicName: org.eclipse.jgit.junit.ssh
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
@@ -33,16 +33,16 @@
  org.apache.sshd.server.subsystem;version="[2.14.0,2.15.0)",
  org.apache.sshd.sftp;version="[2.14.0,2.15.0)",
  org.apache.sshd.sftp.server;version="[2.14.0,2.15.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.junit.ssh;version="7.0.2"
+Export-Package: org.eclipse.jgit.junit.ssh;version="7.1.2"
diff --git a/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF
index 5f840d1..2fb060d 100644
--- a/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.junit.ssh - Sources
 Bundle-SymbolicName: org.eclipse.jgit.junit.ssh.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml
index 067374b..6595b5b 100644
--- a/org.eclipse.jgit.junit.ssh/pom.xml
+++ b/org.eclipse.jgit.junit.ssh/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.ssh</artifactId>
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index c7ae7ce..396df4c 100644
--- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
@@ -3,36 +3,36 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit
 Bundle-SymbolicName: org.eclipse.jgit.junit
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="7.0.2",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.time;version="[7.0.2,7.1.0)",
+Import-Package: org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.dircache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.merge;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="7.1.2",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.io;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.time;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
  org.junit.runners;version="[4.13,5.0.0)",
  org.junit.runners.model;version="[4.13,5.0.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.junit;version="7.0.2";
+Export-Package: org.eclipse.jgit.junit;version="7.1.2";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -45,4 +45,4 @@
    org.junit.runners.model,
    org.junit.runner,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.junit.time;version="7.0.2";uses:="org.eclipse.jgit.util.time"
+ org.eclipse.jgit.junit.time;version="7.1.2";uses:="org.eclipse.jgit.util.time"
diff --git a/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF
index ef76a33..8724466 100644
--- a/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.junit - Sources
 Bundle-SymbolicName: org.eclipse.jgit.junit.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index b63973f..aab23c8 100644
--- a/org.eclipse.jgit.junit/pom.xml
+++ b/org.eclipse.jgit.junit/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit</artifactId>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
index 419fdb1..38f0d0b 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
@@ -18,6 +18,8 @@
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.time.Duration;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -242,6 +244,11 @@ public TimeZone getTimeZone() {
 	}
 
 	@Override
+	public ZoneId getTimeZoneId() {
+		return ZoneOffset.ofHoursMinutes(-3, -30);
+	}
+
+	@Override
 	public Locale getLocale() {
 		return Locale.US;
 	}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java
index c8c56b2..2a482df 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java
@@ -44,7 +44,7 @@ private static Class<?> loadNewClass(Class<?> klass)
 		try {
 			String pathSeparator = System.getProperty("path.separator");
 			String[] classPathEntries = System.getProperty("java.class.path")
-					.split(pathSeparator);
+					.split(pathSeparator, -1);
 			URL[] urls = new URL[classPathEntries.length];
 			for (int i = 0; i < classPathEntries.length; i++) {
 				urls[i] = Paths.get(classPathEntries[i]).toUri().toURL();
diff --git a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
index 3ce8c3e..53f87cb 100644
--- a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.server.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -26,24 +26,24 @@
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.security;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.thread;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.server;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.server.fs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.test;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.server;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.test;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.lfs.server.test/pom.xml b/org.eclipse.jgit.lfs.server.test/pom.xml
index a3a99b5..1568471 100644
--- a/org.eclipse.jgit.lfs.server.test/pom.xml
+++ b/org.eclipse.jgit.lfs.server.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
diff --git a/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
index 70d6029..925ada7 100644
--- a/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
@@ -3,19 +3,19 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.server
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs.server;version="7.0.2";
+Export-Package: org.eclipse.jgit.lfs.server;version="7.1.2";
   uses:="jakarta.servlet.http,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.fs;version="7.0.2";
+ org.eclipse.jgit.lfs.server.fs;version="7.1.2";
   uses:="jakarta.servlet,
    jakarta.servlet.http,
    org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.internal;version="7.0.2";x-internal:=true,
- org.eclipse.jgit.lfs.server.s3;version="7.0.2";
+ org.eclipse.jgit.lfs.server.internal;version="7.1.2";x-internal:=true,
+ org.eclipse.jgit.lfs.server.s3;version="7.1.2";
   uses:="org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib"
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -24,15 +24,15 @@
  jakarta.servlet.annotation;version="[6.0.0,7.0.0)",
  jakarta.servlet.http;version="[6.0.0,7.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http.apache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
diff --git a/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF
index 2c07938..7f52aa9 100644
--- a/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.lfs.server - Sources
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs.server/pom.xml b/org.eclipse.jgit.lfs.server/pom.xml
index 679480b..819450b 100644
--- a/org.eclipse.jgit.lfs.server/pom.xml
+++ b/org.eclipse.jgit.lfs.server/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.server</artifactId>
diff --git a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
index 2d1acdb..38bbaa2 100644
--- a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
@@ -3,28 +3,28 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.attributes;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+Import-Package: org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.attributes;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
  org.junit.runners;version="[4.13,5.0.0)"
-Export-Package: org.eclipse.jgit.lfs.test;version="7.0.2";x-friends:="org.eclipse.jgit.lfs.server.test"
+Export-Package: org.eclipse.jgit.lfs.test;version="7.1.2";x-friends:="org.eclipse.jgit.lfs.server.test"
diff --git a/org.eclipse.jgit.lfs.test/pom.xml b/org.eclipse.jgit.lfs.test/pom.xml
index 79601ff..813823e 100644
--- a/org.eclipse.jgit.lfs.test/pom.xml
+++ b/org.eclipse.jgit.lfs.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.test</artifactId>
diff --git a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
index bb87bab..ec18f81 100644
--- a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
@@ -3,32 +3,32 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs
 Bundle-SymbolicName: org.eclipse.jgit.lfs
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs;version="7.0.2",
- org.eclipse.jgit.lfs.errors;version="7.0.2",
- org.eclipse.jgit.lfs.internal;version="7.0.2";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
- org.eclipse.jgit.lfs.lib;version="7.0.2"
+Export-Package: org.eclipse.jgit.lfs;version="7.1.2",
+ org.eclipse.jgit.lfs.errors;version="7.1.2",
+ org.eclipse.jgit.lfs.internal;version="7.1.2";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
+ org.eclipse.jgit.lfs.lib;version="7.1.2"
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: com.google.gson;version="[2.8.2,3.0.0)",
  com.google.gson.stream;version="[2.8.2,3.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)";resolution:=optional,
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.attributes;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.hooks;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.pack;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.2,7.1.0)"
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)";resolution:=optional,
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.attributes;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.diff;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.dircache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.hooks;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.pack;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.io;version="[7.1.2,7.2.0)"
diff --git a/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
index a1dd5b8..fa88770 100644
--- a/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.lfs - Sources
 Bundle-SymbolicName: org.eclipse.jgit.lfs.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs/pom.xml b/org.eclipse.jgit.lfs/pom.xml
index 80f047a..c435419 100644
--- a/org.eclipse.jgit.lfs/pom.xml
+++ b/org.eclipse.jgit.lfs/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
index 98a3615..26a207f 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
index bf22c74..6ea0915 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml
index 987a925..47cc345 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.gpg.bc"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml
index 7d2f4d9..87ec0ed 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
index 459e77c..3246780 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.http.apache"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
index 7c5b580..1fc7cbf 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
index fe4d8d7..8a83e3c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.junit"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -24,7 +24,7 @@
 
    <requires>
       <import plugin="com.jcraft.jsch"/>
-      <import plugin="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
index 03c53de..3eeb973 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
index e991f99..5eba1c5 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.lfs"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
index 57c62b2..3e90454 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
index aa6ad24..5f0691e 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.pgm"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -35,9 +35,9 @@
          version="0.0.0"/>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
-      <import feature="org.eclipse.jgit.lfs" version="7.0.2" match="equivalent"/>
-      <import feature="org.eclipse.jgit.ssh.apache" version="7.0.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit.lfs" version="7.1.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit.ssh.apache" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
index bb595d6..949a9c8 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
index 2c890e2..adf4e15 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.repository</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
index 79d09f7..b6b5339 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.source"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
index 51bab0c..d67b629 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>org.eclipse.jgit.feature</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
-      <version>7.0.2-SNAPSHOT</version>
+      <version>7.1.2-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
index c84de5d..6546e46 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.ssh.apache"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml
index b135ca0..aa071c9 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml
index 52d0f3d..f795e44 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.ssh.jsch"
       label="%featureName"
-      version="7.0.2.qualifier"
+      version="7.1.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="7.0.2" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.1.2" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml
index fb42902..b93d527 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target
index c1964c9..efae5a1 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.32" sequenceNumber="1729714537">
+<target name="jgit-4.32" sequenceNumber="1731963692">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
@@ -79,7 +79,7 @@
         <dependency>
           <groupId>org.mockito</groupId>
           <artifactId>mockito-core</artifactId>
-          <version>5.12.0</version>
+          <version>5.14.2</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -89,13 +89,13 @@
         <dependency>
           <groupId>net.java.dev.jna</groupId>
           <artifactId>jna</artifactId>
-          <version>5.14.0</version>
+          <version>5.15.0</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>net.java.dev.jna</groupId>
           <artifactId>jna-platform</artifactId>
-          <version>5.14.0</version>
+          <version>5.15.0</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -105,49 +105,49 @@
         <dependency>
           <groupId>org.eclipse.jetty.ee10</groupId>
           <artifactId>jetty-ee10-servlet</artifactId>
-          <version>12.0.10</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-http</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-io</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-security</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-server</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-session</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-util</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-util-ajax</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
@@ -193,13 +193,13 @@
         <dependency>
           <groupId>net.bytebuddy</groupId>
           <artifactId>byte-buddy</artifactId>
-          <version>1.15.0</version>
+          <version>1.15.10</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>net.bytebuddy</groupId>
           <artifactId>byte-buddy-agent</artifactId>
-          <version>1.15.0</version>
+          <version>1.15.10</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -209,25 +209,25 @@
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcpg-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcprov-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcpkix-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcutil-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -269,13 +269,13 @@
         <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
-          <version>3.16.0</version>
+          <version>3.17.0</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>commons-io</groupId>
           <artifactId>commons-io</artifactId>
-          <version>2.16.1</version>
+          <version>2.17.0</version>
           <type>jar</type>
         </dependency>
         <dependency>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target
index 4d9f630..1578e4c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.33" sequenceNumber="1729714538">
+<target name="jgit-4.33" sequenceNumber="1731963694">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
@@ -30,7 +30,7 @@
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/staging/2024-09/"/>
+      <repository location="https://download.eclipse.org/releases/2024-09/"/>
     </location>
     <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
       <dependencies>
@@ -79,7 +79,7 @@
         <dependency>
           <groupId>org.mockito</groupId>
           <artifactId>mockito-core</artifactId>
-          <version>5.12.0</version>
+          <version>5.14.2</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -89,13 +89,13 @@
         <dependency>
           <groupId>net.java.dev.jna</groupId>
           <artifactId>jna</artifactId>
-          <version>5.14.0</version>
+          <version>5.15.0</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>net.java.dev.jna</groupId>
           <artifactId>jna-platform</artifactId>
-          <version>5.14.0</version>
+          <version>5.15.0</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -105,49 +105,49 @@
         <dependency>
           <groupId>org.eclipse.jetty.ee10</groupId>
           <artifactId>jetty-ee10-servlet</artifactId>
-          <version>12.0.10</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-http</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-io</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-security</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-server</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-session</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-util</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-util-ajax</artifactId>
-          <version>12.0.12</version>
+          <version>12.0.15</version>
           <type>jar</type>
         </dependency>
         <dependency>
@@ -193,13 +193,13 @@
         <dependency>
           <groupId>net.bytebuddy</groupId>
           <artifactId>byte-buddy</artifactId>
-          <version>1.15.0</version>
+          <version>1.15.10</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>net.bytebuddy</groupId>
           <artifactId>byte-buddy-agent</artifactId>
-          <version>1.15.0</version>
+          <version>1.15.10</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -209,25 +209,25 @@
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcpg-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcprov-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcpkix-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcutil-jdk18on</artifactId>
-          <version>1.78.1</version>
+          <version>1.79</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -269,13 +269,13 @@
         <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
-          <version>3.16.0</version>
+          <version>3.17.0</version>
           <type>jar</type>
         </dependency>
         <dependency>
           <groupId>commons-io</groupId>
           <artifactId>commons-io</artifactId>
-          <version>2.16.1</version>
+          <version>2.17.0</version>
           <type>jar</type>
         </dependency>
         <dependency>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd
index d01a8a9..74c6878 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd
@@ -3,6 +3,6 @@
 include "orbit/orbit-4.33.tpd"
 include "maven/dependencies.tpd"
 
-location "https://download.eclipse.org/staging/2024-09/" {
+location "https://download.eclipse.org/releases/2024-09/" {
 	org.eclipse.osgi lazy
 }
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.target
new file mode 100644
index 0000000..43678e7
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.target
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.34" sequenceNumber="1731963696">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
+      <unit id="org.apache.ant" version="1.10.15.v20240901-1000"/>
+      <unit id="org.apache.ant.source" version="1.10.15.v20240901-1000"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.junit" version="4.13.2.v20240929-1000"/>
+      <unit id="org.junit.source" version="4.13.2.v20240929-1000"/>
+      <unit id="org.objenesis" version="3.4.0"/>
+      <unit id="org.objenesis.source" version="3.4.0"/>
+      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
+      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
+      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-12"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="org.eclipse.osgi" version="0.0.0"/>
+      <repository location="https://download.eclipse.org/staging/2024-12/"/>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
+      <dependencies>
+        <dependency>
+          <groupId>org.tukaani</groupId>
+          <artifactId>xz</artifactId>
+          <version>1.10</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
+      <dependencies>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-api</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-simple</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-osgi</artifactId>
+          <version>2.14.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-sftp</artifactId>
+          <version>2.14.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
+      <dependencies>
+        <dependency>
+          <groupId>org.mockito</groupId>
+          <artifactId>mockito-core</artifactId>
+          <version>5.14.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
+      <dependencies>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna</artifactId>
+          <version>5.15.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna-platform</artifactId>
+          <version>5.15.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
+      <dependencies>
+        <dependency>
+          <groupId>org.eclipse.jetty.ee10</groupId>
+          <artifactId>jetty-ee10-servlet</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-http</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-io</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-security</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-server</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-session</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util-ajax</artifactId>
+          <version>12.0.15</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+          <version>6.1.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
+      <dependencies>
+        <dependency>
+          <groupId>com.googlecode.javaewah</groupId>
+          <artifactId>JavaEWAH</artifactId>
+          <version>1.2.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
+      <dependencies>
+        <dependency>
+          <groupId>org.hamcrest</groupId>
+          <artifactId>hamcrest</artifactId>
+          <version>2.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
+      <dependencies>
+        <dependency>
+          <groupId>com.google.code.gson</groupId>
+          <artifactId>gson</artifactId>
+          <version>2.11.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
+      <dependencies>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy</artifactId>
+          <version>1.15.10</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy-agent</artifactId>
+          <version>1.15.10</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
+      <dependencies>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpg-jdk18on</artifactId>
+          <version>1.79</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk18on</artifactId>
+          <version>1.79</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk18on</artifactId>
+          <version>1.79</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcutil-jdk18on</artifactId>
+          <version>1.79</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
+      <dependencies>
+        <dependency>
+          <groupId>org.assertj</groupId>
+          <artifactId>assertj-core</artifactId>
+          <version>3.26.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
+      <dependencies>
+        <dependency>
+          <groupId>args4j</groupId>
+          <artifactId>args4j</artifactId>
+          <version>2.37</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
+      <dependencies>
+        <dependency>
+          <groupId>commons-codec</groupId>
+          <artifactId>commons-codec</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-compress</artifactId>
+          <version>1.27.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-lang3</artifactId>
+          <version>3.17.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-io</groupId>
+          <artifactId>commons-io</artifactId>
+          <version>2.17.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-logging</groupId>
+          <artifactId>commons-logging</artifactId>
+          <version>1.3.4</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.tpd
new file mode 100644
index 0000000..4c38371
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.34" with source configurePhase
+
+include "orbit/orbit-4.34.tpd"
+include "maven/dependencies.tpd"
+
+location "https://download.eclipse.org/staging/2024-12/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd
index 42712c0..7b9a397 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd
@@ -20,12 +20,12 @@
 	dependency {
 		groupId = "org.apache.commons"
 		artifactId = "commons-lang3"
-		version = "3.16.0"
+		version = "3.17.0"
 	}
 	dependency {
 		groupId = "commons-io"
 		artifactId = "commons-io"
-		version = "2.16.1"
+		version = "2.17.0"
 	}
 	dependency {
 		groupId = "commons-logging"
@@ -69,22 +69,22 @@
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcpg-jdk18on"
-		version = "1.78.1"
+		version = "1.79"
 	}
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcprov-jdk18on"
-		version = "1.78.1"
+		version = "1.79"
 	}
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcpkix-jdk18on"
-		version = "1.78.1"
+		version = "1.79"
 	}
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcutil-jdk18on"
-		version = "1.78.1"
+		version = "1.79"
 	}
 }
 
@@ -97,12 +97,12 @@
 	dependency {
 		groupId = "net.bytebuddy"
 		artifactId = "byte-buddy"
-		version = "1.15.0"
+		version = "1.15.10"
 	}
 	dependency {
 		groupId = "net.bytebuddy"
 		artifactId = "byte-buddy-agent"
-		version = "1.15.0"
+		version = "1.15.10"
 	}
 }
 
@@ -154,42 +154,42 @@
 	dependency {
 		groupId = "org.eclipse.jetty.ee10"
 		artifactId = "jetty-ee10-servlet"
-		version = "12.0.10"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-http"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-io"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-security"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-server"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-session"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-util"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-util-ajax"
-		version = "12.0.12"
+		version = "12.0.15"
 	}
 	dependency {
 		groupId = "jakarta.servlet"
@@ -207,12 +207,12 @@
 	dependency {
 		groupId = "net.java.dev.jna"
 		artifactId = "jna"
-		version = "5.14.0"
+		version = "5.15.0"
 	}
 	dependency {
 		groupId = "net.java.dev.jna"
 		artifactId = "jna-platform"
-		version = "5.14.0"
+		version = "5.15.0"
 	}
 }
 
@@ -225,7 +225,7 @@
 	dependency {
 		groupId = "org.mockito"
 		artifactId = "mockito-core"
-		version = "5.12.0"
+		version = "5.14.2"
 	}
 }
 
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.34.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.34.tpd
new file mode 100644
index 0000000..15931db
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.34.tpd
@@ -0,0 +1,27 @@
+target "orbit-4.34" with source configurePhase
+// see https://download.eclipse.org/tools/orbit/downloads/
+
+location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-12" {
+	com.jcraft.jsch [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
+	com.jcraft.jsch.source [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
+	com.jcraft.jzlib [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
+	com.jcraft.jzlib.source [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
+	net.i2p.crypto.eddsa [0.3.0,0.3.0]
+	net.i2p.crypto.eddsa.source [0.3.0,0.3.0]
+	org.apache.ant [1.10.15.v20240901-1000,1.10.15.v20240901-1000]
+	org.apache.ant.source [1.10.15.v20240901-1000,1.10.15.v20240901-1000]
+	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
+	org.apache.httpcomponents.httpclient.source [4.5.14,4.5.14]
+	org.apache.httpcomponents.httpcore [4.4.16,4.4.16]
+	org.apache.httpcomponents.httpcore.source [4.4.16,4.4.16]
+	org.hamcrest.core [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
+	org.hamcrest.core.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
+	org.hamcrest.library [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
+	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
+	org.junit [4.13.2.v20240929-1000,4.13.2.v20240929-1000]
+	org.junit.source [4.13.2.v20240929-1000,4.13.2.v20240929-1000]
+	org.objenesis [3.4,3.4]
+	org.objenesis.source [3.4,3.4]
+	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
+	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
+}
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index 5d750fb..37fe6e9 100644
--- a/org.eclipse.jgit.packaging/pom.xml
+++ b/org.eclipse.jgit.packaging/pom.xml
@@ -16,7 +16,7 @@
 
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>jgit.tycho.parent</artifactId>
-  <version>7.0.2-SNAPSHOT</version>
+  <version>7.1.2-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>JGit Tycho Parent</name>
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index 339b969..7d3bc52 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -3,30 +3,30 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm.test
 Bundle-SymbolicName: org.eclipse.jgit.pgm.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.pgm;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.pgm.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.pgm.opt;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.2,7.1.0)",
+Import-Package: org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.diff;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.dircache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.merge;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.pgm;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.pgm.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.pgm.opt;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.io;version="[7.1.2,7.2.0)",
  org.hamcrest.core;bundle-version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.pgm.test/pom.xml b/org.eclipse.jgit.pgm.test/pom.xml
index 8009940..cca0d81 100644
--- a/org.eclipse.jgit.pgm.test/pom.xml
+++ b/org.eclipse.jgit.pgm.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm.test</artifactId>
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java
new file mode 100644
index 0000000..b4d4ea9
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PackRefsTest extends CLIRepositoryTestCase {
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		git.commit().setMessage("initial commit").call();
+	}
+
+	@Test
+	public void tagPacked() throws Exception {
+		git.tag().setName("test").call();
+		git.packRefs().call();
+		assertEquals(Ref.Storage.PACKED,
+				git.getRepository().exactRef("refs/tags/test").getStorage());
+	}
+
+	@Test
+	public void nonTagRefNotPackedWithoutAll() throws Exception {
+		git.branchCreate().setName("test").call();
+		git.packRefs().call();
+		assertEquals(Ref.Storage.LOOSE,
+				git.getRepository().exactRef("refs/heads/test").getStorage());
+	}
+
+	@Test
+	public void nonTagRefPackedWithAll() throws Exception {
+		git.branchCreate().setName("test").call();
+		git.packRefs().setAll(true).call();
+		assertEquals(Ref.Storage.PACKED,
+				git.getRepository().exactRef("refs/heads/test").getStorage());
+	}
+
+	@Test
+	public void refTableCompacted() throws Exception {
+		((FileRepository) git.getRepository()).convertRefStorage(
+				ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false, false);
+
+		git.commit().setMessage("test commit").call();
+		File tableDir = new File(db.getDirectory(), Constants.REFTABLE);
+		File[] reftables = tableDir.listFiles();
+		assertNotNull(reftables);
+		assertTrue(reftables.length > 2);
+
+		git.packRefs().call();
+
+		reftables = tableDir.listFiles();
+		assertNotNull(reftables);
+		assertEquals(2, reftables.length);
+	}
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 0d41cb8..b889686 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm
 Bundle-SymbolicName: org.eclipse.jgit.pgm
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -14,49 +14,49 @@
  org.eclipse.jetty.server.handler;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.archive;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.awtui;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.blame;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.gitrepo;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.io;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.server;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.server.fs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs.server.s3;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.notes;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revplot;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.pack;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.ssh.jsch;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.sshd;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.archive;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.awtui;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.blame;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.diff;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.dircache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.gitrepo;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.io;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.server;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs.server.s3;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.merge;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.notes;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revplot;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.pack;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http.apache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.ssh.jsch;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.sshd;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.io;version="[7.1.2,7.2.0)",
  org.kohsuke.args4j;version="[2.33.0,3.0.0)",
  org.kohsuke.args4j.spi;version="[2.33.0,3.0.0)"
-Export-Package: org.eclipse.jgit.console;version="7.0.2";
+Export-Package: org.eclipse.jgit.console;version="7.1.2";
  uses:="org.eclipse.jgit.transport,
   org.eclipse.jgit.util",
- org.eclipse.jgit.pgm;version="7.0.2";
+ org.eclipse.jgit.pgm;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.awtui,
@@ -68,14 +68,14 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.api,
    javax.swing",
- org.eclipse.jgit.pgm.debug;version="7.0.2";
+ org.eclipse.jgit.pgm.debug;version="7.1.2";
   uses:="org.eclipse.jgit.util.io,
    org.eclipse.jgit.pgm,
    org.eclipse.jetty.servlet",
- org.eclipse.jgit.pgm.internal;version="7.0.2";
+ org.eclipse.jgit.pgm.internal;version="7.1.2";
   x-friends:="org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.test",
- org.eclipse.jgit.pgm.opt;version="7.0.2";
+ org.eclipse.jgit.pgm.opt;version="7.1.2";
   uses:="org.kohsuke.args4j,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
diff --git a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
index 7b5009f..9acb609 100644
--- a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.pgm - Sources
 Bundle-SymbolicName: org.eclipse.jgit.pgm.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index 08d3727..41b0091 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -26,6 +26,7 @@
 org.eclipse.jgit.pgm.Merge
 org.eclipse.jgit.pgm.MergeBase
 org.eclipse.jgit.pgm.MergeTool
+org.eclipse.jgit.pgm.PackRefs
 org.eclipse.jgit.pgm.Push
 org.eclipse.jgit.pgm.ReceivePack
 org.eclipse.jgit.pgm.Reflog
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index a44e19c..b1baa16 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm</artifactId>
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 50ee809..d24b639 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -257,6 +257,7 @@
 usage_Abbrev=Instead of using the default number of hexadecimal digits (which will vary according to the number of objects in the repository with a default of 7) of the abbreviated object name, use <n> digits, or as many digits as needed to form a unique object name. An <n> of 0 will suppress long format, only showing the closest tag.
 usage_addRenormalize=Apply the "clean" process freshly to tracked files to forcibly add them again to the index. This implies -u.
 usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time
+usage_All=Pack all refs, except hidden refs, broken refs, and symbolic refs.
 usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback
 usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR.
 usage_extraArgument=Pass an extra argument to a merge driver. Currently supported are "-X ours" and "-X theirs".
@@ -300,6 +301,7 @@
 usage_MergeBase=Find as good common ancestors as possible for a merge
 usage_MergesTwoDevelopmentHistories=Merges two development histories
 usage_PackKeptObjects=Include objects in packs locked by a ".keep" file when repacking
+usage_PackRefs=Pack heads and tags for efficient repository access
 usage_PreserveOldPacks=Preserve old pack files by moving them into the preserved subdirectory instead of deleting them after repacking
 usage_PrunePreserved=Remove the preserved subdirectory containing previously preserved old pack files before repacking, and before preserving more old pack files
 usage_ReadDirCache= Read the DirCache 100 times
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java
index aacde2f..a29c4d9 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java
@@ -26,11 +26,6 @@ class MergeBase extends TextBuiltin {
 	private boolean all;
 
 	@Argument(index = 0, metaVar = "metaVar_commitish", required = true)
-	void commit_0(final RevCommit c) {
-		commits.add(c);
-	}
-
-	@Argument(index = 1, metaVar = "metaVar_commitish", required = true)
 	private List<RevCommit> commits = new ArrayList<>();
 
 	@Override
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java
new file mode 100644
index 0000000..ee05f5c
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.kohsuke.args4j.Option;
+
+@Command(common = true, usage = "usage_PackRefs")
+class PackRefs extends TextBuiltin {
+	@Option(name = "--all", usage = "usage_All")
+	private boolean all;
+
+	@Override
+	protected void run() {
+		Git git = Git.wrap(db);
+		try {
+			git.packRefs().setProgressMonitor(new TextProgressMonitor(errw))
+					.setAll(all).call();
+		} catch (GitAPIException e) {
+			throw die(e.getMessage(), e);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
index 190cbd7..80d3503 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
@@ -31,6 +31,7 @@
 import org.eclipse.jgit.pgm.TextBuiltin;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.kohsuke.args4j.Argument;
 
@@ -68,7 +69,7 @@ protected void run() throws Exception {
 		ObjectReuseAsIs asis = (ObjectReuseAsIs) reader;
 		ObjectToPack target = asis.newObjectToPack(obj, obj.getType());
 
-		PackWriter pw = new PackWriter(reader) {
+		PackWriter pw = new PackWriter(new PackConfig(), reader) {
 			@Override
 			public boolean select(ObjectToPack otp, StoredObjectRepresentation next) {
 				otp.select(next);
diff --git a/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
index 0cdec4d..46319c9 100644
--- a/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
@@ -2,16 +2,16 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.agent;singleton:=true
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/agent
 Bundle-Vendor: %Bundle-Vendor
-Fragment-Host: org.eclipse.jgit.ssh.apache;bundle-version="[7.0.2,7.1.0)"
+Fragment-Host: org.eclipse.jgit.ssh.apache;bundle-version="[7.1.2,7.2.0)"
 Bundle-ActivationPolicy: lazy
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.agent
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.transport.sshd;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)"
+Import-Package: org.eclipse.jgit.transport.sshd;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)"
 Require-Bundle: com.sun.jna;bundle-version="[5.8.0,6.0.0)",
  com.sun.jna.platform;bundle-version="[5.8.0,6.0.0)"
-Export-Package: org.eclipse.jgit.internal.transport.sshd.agent.connector;version="7.0.2";x-internal:=true
+Export-Package: org.eclipse.jgit.internal.transport.sshd.agent.connector;version="7.1.2";x-internal:=true
diff --git a/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
index d4fea05..773bd02 100644
--- a/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.apache.agent - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.agent.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache.agent;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache.agent;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache.agent/pom.xml b/org.eclipse.jgit.ssh.apache.agent/pom.xml
index 9a44834..85ac2a9 100644
--- a/org.eclipse.jgit.ssh.apache.agent/pom.xml
+++ b/org.eclipse.jgit.ssh.apache.agent/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache.agent</artifactId>
diff --git a/org.eclipse.jgit.ssh.apache.test/.classpath b/org.eclipse.jgit.ssh.apache.test/.classpath
index 6fdb99a..5be47af 100644
--- a/org.eclipse.jgit.ssh.apache.test/.classpath
+++ b/org.eclipse.jgit.ssh.apache.test/.classpath
@@ -11,5 +11,10 @@
 			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>
+	<classpathentry kind="src" path="tst-rsrc">
+		<attributes>
+			<attribute name="test" value="true"/>
+		</attributes>
+	</classpathentry>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/org.eclipse.jgit.ssh.apache.test/.gitattributes b/org.eclipse.jgit.ssh.apache.test/.gitattributes
new file mode 100644
index 0000000..b5b9375
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/.gitattributes
@@ -0,0 +1,2 @@
+/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle binary
+/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl* binary
diff --git a/org.eclipse.jgit.ssh.apache.test/BUILD b/org.eclipse.jgit.ssh.apache.test/BUILD
index b384464..dfc059f 100644
--- a/org.eclipse.jgit.ssh.apache.test/BUILD
+++ b/org.eclipse.jgit.ssh.apache.test/BUILD
@@ -1,19 +1,49 @@
 load(
+    "@com_googlesource_gerrit_bazlets//tools:genrule2.bzl",
+    "genrule2",
+)
+load(
     "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
     "junit_tests",
 )
 
+DEPS = [
+    "//lib:eddsa",
+    "//lib:junit",
+    "//lib:slf4j-api",
+    "//lib:sshd-osgi",
+    "//lib:sshd-sftp",
+    "//org.eclipse.jgit:jgit",
+    "//org.eclipse.jgit.junit:junit",
+    "//org.eclipse.jgit.junit.ssh:junit-ssh",
+    "//org.eclipse.jgit.ssh.apache:ssh-apache",
+]
+
+HELPERS = ["tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java"]
+
 junit_tests(
     name = "sshd_apache",
-    srcs = glob(["tst/**/*.java"]),
+    srcs = glob(
+        ["tst/**/*.java"],
+        exclude = HELPERS,
+    ),
     tags = ["sshd"],
-    deps = [
-        "//lib:eddsa",
-        "//lib:junit",
-        "//lib:sshd-osgi",
-        "//lib:sshd-sftp",
-        "//org.eclipse.jgit:jgit",
-        "//org.eclipse.jgit.junit.ssh:junit-ssh",
-        "//org.eclipse.jgit.ssh.apache:ssh-apache",
+    runtime_deps = [":tst_rsrc"],
+    deps = DEPS + [
+        ":helpers",
     ],
 )
+
+java_library(
+    name = "helpers",
+    testonly = 1,
+    srcs = HELPERS,
+    deps = DEPS,
+)
+
+genrule2(
+    name = "tst_rsrc",
+    srcs = glob(["tst-rsrc/**"]),
+    outs = ["tst_rsrc.jar"],
+    cmd = "tar cf - $(SRCS) | tar -C $$TMP --strip-components=2 -xf - && cd $$TMP && zip -qr $$ROOT/$@ .",
+)
diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
index cc382fb..21619de 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -17,22 +17,28 @@
  org.apache.sshd.common.keyprovider;version="[2.14.0,2.15.0)",
  org.apache.sshd.common.session;version="[2.14.0,2.15.0)",
  org.apache.sshd.common.signature;version="[2.14.0,2.15.0)",
+ org.apache.sshd.common.util.buffer;version="[2.14.0,2.15.0)",
  org.apache.sshd.common.util.net;version="[2.14.0,2.15.0)",
  org.apache.sshd.common.util.security;version="[2.14.0,2.15.0)",
  org.apache.sshd.core;version="[2.14.0,2.15.0)",
  org.apache.sshd.server;version="[2.14.0,2.15.0)",
  org.apache.sshd.server.forward;version="[2.14.0,2.15.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit.ssh;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.sshd;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.sshd.agent;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.signing.ssh;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit.ssh;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.sshd;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.sshd.agent;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
- org.junit.runner;version="[4.13,5.0.0)"
+ org.junit.rules;version="[4.13.0,5.0.0)",
+ org.junit.runner;version="[4.13,5.0.0)",
+ org.junit.runners;version="[4.13.0,5.0.0)",
+ org.slf4j;version="[1.7.0,2.0.0)"
diff --git a/org.eclipse.jgit.ssh.apache.test/pom.xml b/org.eclipse.jgit.ssh.apache.test/pom.xml
index 624104d..c53adb7 100644
--- a/org.eclipse.jgit.ssh.apache.test/pom.xml
+++ b/org.eclipse.jgit.ssh.apache.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
@@ -91,6 +91,12 @@
     <sourceDirectory>src/</sourceDirectory>
     <testSourceDirectory>tst/</testSourceDirectory>
 
+    <testResources>
+      <testResource>
+        <directory>tst-rsrc/</directory>
+      </testResource>
+    </testResources>
+
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/allowed_signers b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/allowed_signers
new file mode 100644
index 0000000..ec74409
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/allowed_signers
@@ -0,0 +1,2 @@
+tester@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO
+*@example.com cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBV
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key
new file mode 100644
index 0000000..b8de8c3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAJAhCMgzIQjI
+MwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQ
+AAAEBmcXpast20+B4IzA0Xex2CKYiiWJj3NFJ5F0kil113vcdEl+iOTEbf1RC3uicECtid
++SaIMsAw7wrlWhOQTyBVAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub
new file mode 100644
index 0000000..842415b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBV
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2
new file mode 100644
index 0000000..a4af047
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAJAa2jfBGto3
+wQAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgw
+AAAEBothGMqFaA5aTO8MLx9wm1oDRfzQCSsu7uJwrOiUFTTTtLjJwgHqhPHhQ3yX0367TX
+pSAzOSttPsT7ZdsbfmODAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub
new file mode 100644
index 0000000..e46c87e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDtLjJwgHqhPHhQ3yX0367TXpSAzOSttPsT7ZdsbfmOD
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert
new file mode 100644
index 0000000..9da63ec
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINFZ5NKywAWh1G1P6BiBKArmYKs1BDhJBOawJKlS29VXAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAEAAAABAAAADGV4cGlyZWRfY2VydAAAAAAAAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgx0SX6I5MRt/VELe6JwQK2J35JogywDDvCuVaE5BPIFUAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNf8i5dhRqWRe06epIRrZ5V+QZHq3ZrlJtlx98UJya9GAeCrJ5oHwBjr5O5TL5wNJS5Hz+T1qsJNFU9d1wdcuwI=
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert
new file mode 100644
index 0000000..101e374
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILzuED1RSloB/enTghTEKSACVOuEARP0f8UVXSRwEXN6AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAIAAAABAAAADW5vX3ByaW5jaXBhbHMAAAAAAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBwEQ2D0OHn4QDHnsINlgWUWpmhukseQCJu3Adulz28fFtewp1LLqkBy50wR6vJe1ifYbY4hzReXOSyoTmHSXEN
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert
new file mode 100644
index 0000000..752fee1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIHNW2bzSS61lvgHippv3Ymx4cVEAXBVCb8lFXHnVpsSyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAYAAAABAAAAB3Rlc3RlcjIAAAAWAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAFMAAAALc3NoLWVkMjU1MTkAAABAuJ8zBazcaYTbUEr9QtoYox0MkVBg+8LANxJxc885M2vmg9yPHpTfV/emupqhBwuYcPJSskTxl7WX4TUNvhMsAA==
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert
new file mode 100644
index 0000000..15825f6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGKXzyrvDzj9ObQ4SuzqytK6nomOV8DhgdzODfWuup1sAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAQAAAABAAAABW90aGVyAAAAFQAAABFvdGhlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABA1ycFqWehyC6pIISEkXSTtHbatLWl9HHAoUFouQiDdubAnMDRSkyHipXR62rq+8yEAvtqm1mXBzO8nLalkF9xAA==
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert
new file mode 100644
index 0000000..a2b241c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICSl1xsyTWb23YlKo21musxOzj4L4eD2coTkHbBw2uOyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAMAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDyjzq/0Egm1OxwrvqPZKUihE3w357Ji9Nd3j7VnUuvSYTXAdB9P0E+a2hyCcemmsil1MsvWTiCSSOsrHVB6FEO
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert
new file mode 100644
index 0000000..5f7164a
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFmWKr9gNSQT0vna7k3uOyUF9CTcMGw2zxTFBf2Ev8TzAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAgAAAABAAAABnRlc3RlcgAAACkAAAAPZm9vQGV4YW1wbGUuY29tAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABAqlSX2GzLz5U+hN/gF9UUyAkE6h5BgVFYhsyf1MR/B7Hoxa29wGLbJpUplrqEHMxoud2zfH2Nhj00unc3lr5bBA== ./signing_key.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl
new file mode 100644
index 0000000..9469340
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-all b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-all
new file mode 100644
index 0000000..6f744c3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-all
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-ca b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-ca
new file mode 100644
index 0000000..84a8bc6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-ca
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-cert
new file mode 100644
index 0000000..26f29b2
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-cert
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-empty b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-empty
new file mode 100644
index 0000000..78e5187
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-empty
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-hash b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-hash
new file mode 100644
index 0000000..cdd1351
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-hash
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid
new file mode 100644
index 0000000..1a65243
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid-wild b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid-wild
new file mode 100644
index 0000000..9ba549f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid-wild
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keys b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keys
new file mode 100644
index 0000000..8dd496d
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keys
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial
new file mode 100644
index 0000000..9965e2e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial-wild b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial-wild
new file mode 100644
index 0000000..aefd2b1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial-wild
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha1 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha1
new file mode 100644
index 0000000..3928543
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha1
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha256 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha256
new file mode 100644
index 0000000..cdd1351
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha256
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-text b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-text
new file mode 100644
index 0000000..77ddd5e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-text
@@ -0,0 +1,11 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001
new file mode 100644
index 0000000..893fd5e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAtvUGe9h7cgnWjaOqdhh93yaVMp8JL7PAHMJ+uhu2E4QAAAIhUa4KiVGuC
+ogAAAAtzc2gtZWQyNTUxOQAAACAtvUGe9h7cgnWjaOqdhh93yaVMp8JL7PAHMJ+uhu2E4Q
+AAAECKgy+3FBgpdfxjOtNy9TamhadMWSyPlPiwu06mYVReyS29QZ72HtyCdaNo6p2GH3fJ
+pUynwkvs8Acwn66G7YThAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001-cert.pub
new file mode 100644
index 0000000..e2bcd25
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIE1UUsQ+sncsuST6eGe3B5Se7purqhGcWrkyIwUnQM/jAAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YThAAAAAAAAAAEAAAABAAAACXJldm9rZWQgMQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQCjDATJVQs3odl9fsqaxyx/18qrodZEDyYZAsdqg0GMx8CvLYt4xHENyVm7kyBRxOeh3EKfII0WFoYCV4mGZ/wU= ./tst-keys/revoked-0001.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001.pub
new file mode 100644
index 0000000..f561982
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004
new file mode 100644
index 0000000..e50a4fe
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC9GQuH6oGwYETnl9bDhD6+LjFTXz3Setm1aP870jEUMQAAAIiQzZzikM2c
+4gAAAAtzc2gtZWQyNTUxOQAAACC9GQuH6oGwYETnl9bDhD6+LjFTXz3Setm1aP870jEUMQ
+AAAEBpn5dxbvHhqAsSVN3IqRwzbFFgOhdmpkOP+nvoKq+rSr0ZC4fqgbBgROeX1sOEPr4u
+MVNfPdJ62bVo/zvSMRQxAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004-cert.pub
new file mode 100644
index 0000000..8e92fa7
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIC5jMLPDlEVbyPU/Icb04BF5jxN+OT8kpuO5c0CV6/AYAAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQxAAAAAAAAAAQAAAABAAAACXJldm9rZWQgNAAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQOH4yNn7+zyvsCV8BCoop5xYv4uFk27VZRjmscuy3J66KNwLay9XkvkRNArDaWBwH47dmkcU7F6fLLpY4vN2jgM= ./tst-keys/revoked-0004.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004.pub
new file mode 100644
index 0000000..1d7fe7f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010
new file mode 100644
index 0000000..fb457df
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBuSB/U+xWOO1SQ1KMjpQf4qgjeKTvHYDq8XJZijeUecAAAAIgvtSiML7Uo
+jAAAAAtzc2gtZWQyNTUxOQAAACBuSB/U+xWOO1SQ1KMjpQf4qgjeKTvHYDq8XJZijeUecA
+AAAECI2si7/SGjMM1UyhrFPXx4laQIfFUsb1+yfXKwQyeOXW5IH9T7FY47VJDUoyOlB/iq
+CN4pO8dgOrxclmKN5R5wAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010-cert.pub
new file mode 100644
index 0000000..9492f88
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIN2arXaBzVIdxAFfU+XU1Uc788HKlDH3tOLdDtcoORLmAAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5wAAAAAAAAAAoAAAABAAAACnJldm9rZWQgMTAAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDwhgQsYOG/eKf8EfH+fAmEW+88/ZJCmxAExEFPxkGL59waZcGiOJqquTKiqN5Kod8hpUrvZywrA0tjrRkYw8wH ./tst-keys/revoked-0010.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010.pub
new file mode 100644
index 0000000..37a0d84
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050
new file mode 100644
index 0000000..b02e9df
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDY4RuPhJ0MvGBy7HxPPiEMDVmVZ9RgWF++acMQQmRpsgAAAIgCZLe5AmS3
+uQAAAAtzc2gtZWQyNTUxOQAAACDY4RuPhJ0MvGBy7HxPPiEMDVmVZ9RgWF++acMQQmRpsg
+AAAEB9Q6rpWK04mQDoeKSB2I7p/rb8pu00ClhR+vRATl4TYdjhG4+EnQy8YHLsfE8+IQwN
+WZVn1GBYX75pwxBCZGmyAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050-cert.pub
new file mode 100644
index 0000000..90bb86f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIecNj2Es6VfyCrhol4swP9lutvphd3seh+/b2LpD0EsAAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmyAAAAAAAAADIAAAABAAAACnJldm9rZWQgNTAAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEA2q8tCXV8FXkB0QWnFNWfCL7zz5jCXL9ZQADM1DaGi8oUU/dxmlQtWgMxuu5vNuvOYQGPDcBLj+by8VqAdvZMP ./tst-keys/revoked-0050.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050.pub
new file mode 100644
index 0000000..f3ad249
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090
new file mode 100644
index 0000000..efa3d5e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCV5vqoE7PrDvvc76GQcZEA9/Udq0KkAlXs8UKBoyrouQAAAIg3mgznN5oM
+5wAAAAtzc2gtZWQyNTUxOQAAACCV5vqoE7PrDvvc76GQcZEA9/Udq0KkAlXs8UKBoyrouQ
+AAAEAkRynGUH9n5hcp/S1WALvuIEDtbkMi2A7yNWze0o4gWpXm+qgTs+sO+9zvoZBxkQD3
+9R2rQqQCVezxQoGjKui5AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090-cert.pub
new file mode 100644
index 0000000..26e61e0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIOjIztpPiaKY0hztHWtWpX+4LEoyy8qYPPT277K3bykSAAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5AAAAAAAAAFoAAAABAAAACnJldm9rZWQgOTAAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBUaAWyv/jZrbrCO5zw2HuZcWYBig8R2jdvkKr5yzWMWEVRtn97gnAUsIGxkgUnUAs3B2En2FH2NaicC1F1n3sF ./tst-keys/revoked-0090.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090.pub
new file mode 100644
index 0000000..e51b88c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500
new file mode 100644
index 0000000..900d444
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC/PmkCo2vkm6sX1ketRQnLGwcNo2hfyh73KnT9hW6ekAAAAIhDam0PQ2pt
+DwAAAAtzc2gtZWQyNTUxOQAAACC/PmkCo2vkm6sX1ketRQnLGwcNo2hfyh73KnT9hW6ekA
+AAAED606GrYWlY7TOXcr8vAr3fjMtCtetdpwFHi2pzgf2Bbb8+aQKja+SbqxfWR61FCcsb
+Bw2jaF/KHvcqdP2Fbp6QAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500-cert.pub
new file mode 100644
index 0000000..0709618
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIEblAg4b1eJ5KnT7KvYoOfe24La+nAKKLIYdsR6CdreAAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6QAAAAAAAAAfQAAAABAAAAC3Jldm9rZWQgNTAwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAc0WEuRfi9LG9uTfKY4Dh5MJCHUG7Dqp1J4S4Gs1iOzFX2YKgYXc0O+9j3jJ5/fB4z960Y1AxYR4TWEo1pNjzBQ== ./tst-keys/revoked-0500.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500.pub
new file mode 100644
index 0000000..13d1aa4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510
new file mode 100644
index 0000000..a58675e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACADi+yFC2pU9iM6PU4EX9bdhzaWeVgmSR+aEQnfSytwTQAAAIgigF2AIoBd
+gAAAAAtzc2gtZWQyNTUxOQAAACADi+yFC2pU9iM6PU4EX9bdhzaWeVgmSR+aEQnfSytwTQ
+AAAEBWpyFpK0a+cdNPFMsvHTHtjBJpX4aMHxBAcEPN8hnpWAOL7IULalT2Izo9TgRf1t2H
+NpZ5WCZJH5oRCd9LK3BNAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510-cert.pub
new file mode 100644
index 0000000..1431af3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAII8u8ho0YtDyXWYKv4WeOXSaRUxU8sUV0dQujB2J9VLaAAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BNAAAAAAAAAf4AAAABAAAAC3Jldm9rZWQgNTEwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABA3aijJnt8mJ8vLtr7H2PBVJHtNJpL6MQZNXHC6svzygIqZwEq3tDHGR00TPHaCYAqDEXQZysONciOQtQHzKXuBw== ./tst-keys/revoked-0510.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510.pub
new file mode 100644
index 0000000..33ad644
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520
new file mode 100644
index 0000000..630316c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBlWknnYT5Jdh68dZzjik9hOdgA7AXm2sMLPOzTdfj+ngAAAIghEm1OIRJt
+TgAAAAtzc2gtZWQyNTUxOQAAACBlWknnYT5Jdh68dZzjik9hOdgA7AXm2sMLPOzTdfj+ng
+AAAEDfVYURudvfzK3ZFx6T2O1CWi0emOZ0MYPcDzUVlu1WmGVaSedhPkl2Hrx1nOOKT2E5
+2ADsBebawws87NN1+P6eAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520-cert.pub
new file mode 100644
index 0000000..b290943
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAID/r9T2Sv0NGmlcHl6Fw8rVPIupmsqwq3WAG1NvW7WRcAAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6eAAAAAAAAAggAAAABAAAAC3Jldm9rZWQgNTIwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAF8zkeAqwtlxF4iy4mDEHkzVaRqcS0sZ57gcZBWGn/peGFy3MpSxlFQM/IC2pNZ7GuCVSIPV6rRLJC65YMMOEDQ== ./tst-keys/revoked-0520.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520.pub
new file mode 100644
index 0000000..fc13d37
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550
new file mode 100644
index 0000000..5e671b4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAscmsY+6UVQ3of4MSXpvQS4aFEChpylx3w6wfxrQtSgQAAAIj/9GKZ//Ri
+mQAAAAtzc2gtZWQyNTUxOQAAACAscmsY+6UVQ3of4MSXpvQS4aFEChpylx3w6wfxrQtSgQ
+AAAEDKC3eEgvCMy86rktq7VU1YQjjKY1iDFPVxWgKKcGJKkyxyaxj7pRVDeh/gxJem9BLh
+oUQKGnKXHfDrB/GtC1KBAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550-cert.pub
new file mode 100644
index 0000000..f529a91
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIF9q+Cg+9DSKt09eW1NXqVC4dZ3v80sZIYtc0/yqHRb+AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KBAAAAAAAAAiYAAAABAAAAC3Jldm9rZWQgNTUwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAovTuFOXLNCc4hQcI2hatXe2hbBQYbcnUo2BNdJ9EvIOsH/T0DzzEfRQajMQ+QD6oujIx7fb1Z2sRVPOAb3AcBg== ./tst-keys/revoked-0550.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550.pub
new file mode 100644
index 0000000..e09316a
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799
new file mode 100644
index 0000000..8edd736
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA/ehirY+MDfjsL60LID9+30FVF04QjC+/eqqhTS1QwgAAAAIjdntzR3Z7c
+0QAAAAtzc2gtZWQyNTUxOQAAACA/ehirY+MDfjsL60LID9+30FVF04QjC+/eqqhTS1QwgA
+AAAEDQEb+IFCIz+yvkhmrOQ85GafOm9ra0oNRontpox62UTj96GKtj4wN+OwvrQsgP37fQ
+VUXThCML796qqFNLVDCAAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799-cert.pub
new file mode 100644
index 0000000..80312fb
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIC1z1LkrZhMz1mBWPU8sJIuH59v+ig4OK/B4/x8jLAtUAAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCAAAAAAAAAAx8AAAABAAAAC3Jldm9rZWQgNzk5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABASNkJSbdRDARfgbqPOnuES0o6m6VZ7RC2XLPm3uwTqCvMqtHbFvq9etMddSUIR4XXah6ef+O7CJDk/Yjpkn+2CA== ./tst-keys/revoked-0799.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799.pub
new file mode 100644
index 0000000..1f0556c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999
new file mode 100644
index 0000000..f05a1e4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBIdwRXE815oNRYuS71olA1jkbB81YVApvHpKxCRuvugAAAAIgzBpObMwaT
+mwAAAAtzc2gtZWQyNTUxOQAAACBIdwRXE815oNRYuS71olA1jkbB81YVApvHpKxCRuvugA
+AAAECxY5wx3XKIhMT+ajMZXPl51x8rkCPBq6gUgZV3Uqpu7Eh3BFcTzXmg1Fi5LvWiUDWO
+RsHzVhUCm8ekrEJG6+6AAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999-cert.pub
new file mode 100644
index 0000000..4aedb77
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGt3nV/XJmtz9sQGP2fiZiKOH7mkPhezN3S+8TnsVcQjAAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6AAAAAAAAAA+cAAAABAAAAC3Jldm9rZWQgOTk5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAvLVCRCs7CV0JSXYL8ge4iRxL4y48bYuvu3YimKZDg7NdCXqw/jkaCsxJykRzb/xVnQDoNVCQQuzydt/I13FdBA== ./tst-keys/revoked-0999.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999.pub
new file mode 100644
index 0000000..c837fe0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca
new file mode 100644
index 0000000..47e01fb
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAIgok4I2KJOC
+NgAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzg
+AAAEAEN+knz2qOyj+jbY+SJSHYQhlJoB1u9jLqoQoiAerI3hbReZevLKczhayKUADRdAvZ
+5DXVzAJpQkcB4MPdQu/OAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca.pub
new file mode 100644
index 0000000..2b92f89
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/O 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2
new file mode 100644
index 0000000..770ceee
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDsEBCX5jBwggkpt4XZXct1fOhDBuvgLL0KMpGoHRtj9wAAAIjMEwOtzBMD
+rQAAAAtzc2gtZWQyNTUxOQAAACDsEBCX5jBwggkpt4XZXct1fOhDBuvgLL0KMpGoHRtj9w
+AAAEAurE2/d7VhoEJeNFdDnVS7lpBRoMe/zAjA8dJRP1Z/I+wQEJfmMHCCCSm3hdldy3V8
+6EMG6+AsvQoykagdG2P3AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2.pub
new file mode 100644
index 0000000..a177fd0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOwQEJfmMHCCCSm3hdldy3V86EMG6+AsvQoykagdG2P3 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-hash b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-hash
new file mode 100644
index 0000000..c6f2361
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-hash
@@ -0,0 +1,11 @@
+hash: SHA256:RvNFBEc/N9jsm3toDkitgr/wnWu/6qWBHo4Xmh5ZUpM
+hash: SHA256:qu2IwCnItWWX+orXv0rjCeT4i++2O6ViTzLye6kyWzU
+hash: SHA256:qQTACAkAJxYk1zvSQ+Rx9wa2IuOFJKtaEy/XwxM89J0
+hash: SHA256:Fe4GdmipzulS9oMB/h3U69tSm5wil6bTUKSJCT+Jf3E
+hash: SHA256:esUK/whZ5oJeRFNeOrHK1bbx9dKC+nRITZ7up7HJaGA
+hash: SHA256:xkii+r6t9rEBFYkx1b3dGNXzEs69M5NUMfHP05ypSdI
+hash: SHA256:lZrSycKcBNvUafU9y4R0EEbDaQWqMFvIGM9M+VKt2zk
+hash: SHA256:/2bgZOiYEH2UVahUllNaQ5P0advEB7liCPkp+aNVKDk
+hash: SHA256:He3c0W5o/P1I0pK5/VusqD5V6duAMeZl6f+6Yy5P1z0
+hash: SHA256:5V5Xw2lgcAGR8dO9cbgRmCNlhcCsBBv/hmEstKsqKr4
+hash: SHA256:T7s26JPzzRP2WHOcw3OjLwWo8ZZTkfo2jBCrRfJ6BR4
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-keyid b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-keyid
new file mode 100644
index 0000000..592ddb4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-keyid
@@ -0,0 +1,512 @@
+id: revoked 1
+id: revoked 2
+id: revoked 3
+id: revoked 4
+id: revoked 10
+id: revoked 15
+id: revoked 30
+id: revoked 50
+id: revoked 90
+id: revoked 300
+id: revoked 301
+id: revoked 302
+id: revoked 303
+id: revoked 304
+id: revoked 305
+id: revoked 306
+id: revoked 307
+id: revoked 308
+id: revoked 309
+id: revoked 310
+id: revoked 311
+id: revoked 312
+id: revoked 313
+id: revoked 314
+id: revoked 315
+id: revoked 316
+id: revoked 317
+id: revoked 318
+id: revoked 319
+id: revoked 320
+id: revoked 321
+id: revoked 322
+id: revoked 323
+id: revoked 324
+id: revoked 325
+id: revoked 326
+id: revoked 327
+id: revoked 328
+id: revoked 329
+id: revoked 330
+id: revoked 331
+id: revoked 332
+id: revoked 333
+id: revoked 334
+id: revoked 335
+id: revoked 336
+id: revoked 337
+id: revoked 338
+id: revoked 339
+id: revoked 340
+id: revoked 341
+id: revoked 342
+id: revoked 343
+id: revoked 344
+id: revoked 345
+id: revoked 346
+id: revoked 347
+id: revoked 348
+id: revoked 349
+id: revoked 350
+id: revoked 351
+id: revoked 352
+id: revoked 353
+id: revoked 354
+id: revoked 355
+id: revoked 356
+id: revoked 357
+id: revoked 358
+id: revoked 359
+id: revoked 360
+id: revoked 361
+id: revoked 362
+id: revoked 363
+id: revoked 364
+id: revoked 365
+id: revoked 366
+id: revoked 367
+id: revoked 368
+id: revoked 369
+id: revoked 370
+id: revoked 371
+id: revoked 372
+id: revoked 373
+id: revoked 374
+id: revoked 375
+id: revoked 376
+id: revoked 377
+id: revoked 378
+id: revoked 379
+id: revoked 380
+id: revoked 381
+id: revoked 382
+id: revoked 383
+id: revoked 384
+id: revoked 385
+id: revoked 386
+id: revoked 387
+id: revoked 388
+id: revoked 389
+id: revoked 390
+id: revoked 391
+id: revoked 392
+id: revoked 393
+id: revoked 394
+id: revoked 395
+id: revoked 396
+id: revoked 397
+id: revoked 398
+id: revoked 399
+id: revoked 400
+id: revoked 401
+id: revoked 402
+id: revoked 403
+id: revoked 404
+id: revoked 405
+id: revoked 406
+id: revoked 407
+id: revoked 408
+id: revoked 409
+id: revoked 410
+id: revoked 411
+id: revoked 412
+id: revoked 413
+id: revoked 414
+id: revoked 415
+id: revoked 416
+id: revoked 417
+id: revoked 418
+id: revoked 419
+id: revoked 420
+id: revoked 421
+id: revoked 422
+id: revoked 423
+id: revoked 424
+id: revoked 425
+id: revoked 426
+id: revoked 427
+id: revoked 428
+id: revoked 429
+id: revoked 430
+id: revoked 431
+id: revoked 432
+id: revoked 433
+id: revoked 434
+id: revoked 435
+id: revoked 436
+id: revoked 437
+id: revoked 438
+id: revoked 439
+id: revoked 440
+id: revoked 441
+id: revoked 442
+id: revoked 443
+id: revoked 444
+id: revoked 445
+id: revoked 446
+id: revoked 447
+id: revoked 448
+id: revoked 449
+id: revoked 450
+id: revoked 451
+id: revoked 452
+id: revoked 453
+id: revoked 454
+id: revoked 455
+id: revoked 456
+id: revoked 457
+id: revoked 458
+id: revoked 459
+id: revoked 460
+id: revoked 461
+id: revoked 462
+id: revoked 463
+id: revoked 464
+id: revoked 465
+id: revoked 466
+id: revoked 467
+id: revoked 468
+id: revoked 469
+id: revoked 470
+id: revoked 471
+id: revoked 472
+id: revoked 473
+id: revoked 474
+id: revoked 475
+id: revoked 476
+id: revoked 477
+id: revoked 478
+id: revoked 479
+id: revoked 480
+id: revoked 481
+id: revoked 482
+id: revoked 483
+id: revoked 484
+id: revoked 485
+id: revoked 486
+id: revoked 487
+id: revoked 488
+id: revoked 489
+id: revoked 490
+id: revoked 491
+id: revoked 492
+id: revoked 493
+id: revoked 494
+id: revoked 495
+id: revoked 496
+id: revoked 497
+id: revoked 498
+id: revoked 500
+id: revoked 501
+id: revoked 502
+id: revoked 503
+id: revoked 504
+id: revoked 505
+id: revoked 506
+id: revoked 507
+id: revoked 508
+id: revoked 509
+id: revoked 510
+id: revoked 511
+id: revoked 512
+id: revoked 513
+id: revoked 514
+id: revoked 515
+id: revoked 516
+id: revoked 517
+id: revoked 518
+id: revoked 519
+id: revoked 520
+id: revoked 521
+id: revoked 522
+id: revoked 523
+id: revoked 524
+id: revoked 525
+id: revoked 526
+id: revoked 527
+id: revoked 528
+id: revoked 529
+id: revoked 530
+id: revoked 531
+id: revoked 532
+id: revoked 533
+id: revoked 534
+id: revoked 535
+id: revoked 536
+id: revoked 537
+id: revoked 538
+id: revoked 539
+id: revoked 540
+id: revoked 541
+id: revoked 542
+id: revoked 543
+id: revoked 544
+id: revoked 545
+id: revoked 546
+id: revoked 547
+id: revoked 548
+id: revoked 549
+id: revoked 550
+id: revoked 551
+id: revoked 552
+id: revoked 553
+id: revoked 554
+id: revoked 555
+id: revoked 556
+id: revoked 557
+id: revoked 558
+id: revoked 559
+id: revoked 560
+id: revoked 561
+id: revoked 562
+id: revoked 563
+id: revoked 564
+id: revoked 565
+id: revoked 566
+id: revoked 567
+id: revoked 568
+id: revoked 569
+id: revoked 570
+id: revoked 571
+id: revoked 572
+id: revoked 573
+id: revoked 574
+id: revoked 575
+id: revoked 576
+id: revoked 577
+id: revoked 578
+id: revoked 579
+id: revoked 580
+id: revoked 581
+id: revoked 582
+id: revoked 583
+id: revoked 584
+id: revoked 585
+id: revoked 586
+id: revoked 587
+id: revoked 588
+id: revoked 589
+id: revoked 590
+id: revoked 591
+id: revoked 592
+id: revoked 593
+id: revoked 594
+id: revoked 595
+id: revoked 596
+id: revoked 597
+id: revoked 598
+id: revoked 599
+id: revoked 600
+id: revoked 601
+id: revoked 602
+id: revoked 603
+id: revoked 604
+id: revoked 605
+id: revoked 606
+id: revoked 607
+id: revoked 608
+id: revoked 609
+id: revoked 610
+id: revoked 611
+id: revoked 612
+id: revoked 613
+id: revoked 614
+id: revoked 615
+id: revoked 616
+id: revoked 617
+id: revoked 618
+id: revoked 619
+id: revoked 620
+id: revoked 621
+id: revoked 622
+id: revoked 623
+id: revoked 624
+id: revoked 625
+id: revoked 626
+id: revoked 627
+id: revoked 628
+id: revoked 629
+id: revoked 630
+id: revoked 631
+id: revoked 632
+id: revoked 633
+id: revoked 634
+id: revoked 635
+id: revoked 636
+id: revoked 637
+id: revoked 638
+id: revoked 639
+id: revoked 640
+id: revoked 641
+id: revoked 642
+id: revoked 643
+id: revoked 644
+id: revoked 645
+id: revoked 646
+id: revoked 647
+id: revoked 648
+id: revoked 649
+id: revoked 650
+id: revoked 651
+id: revoked 652
+id: revoked 653
+id: revoked 654
+id: revoked 655
+id: revoked 656
+id: revoked 657
+id: revoked 658
+id: revoked 659
+id: revoked 660
+id: revoked 661
+id: revoked 662
+id: revoked 663
+id: revoked 664
+id: revoked 665
+id: revoked 666
+id: revoked 667
+id: revoked 668
+id: revoked 669
+id: revoked 670
+id: revoked 671
+id: revoked 672
+id: revoked 673
+id: revoked 674
+id: revoked 675
+id: revoked 676
+id: revoked 677
+id: revoked 678
+id: revoked 679
+id: revoked 680
+id: revoked 681
+id: revoked 682
+id: revoked 683
+id: revoked 684
+id: revoked 685
+id: revoked 686
+id: revoked 687
+id: revoked 688
+id: revoked 689
+id: revoked 690
+id: revoked 691
+id: revoked 692
+id: revoked 693
+id: revoked 694
+id: revoked 695
+id: revoked 696
+id: revoked 697
+id: revoked 698
+id: revoked 699
+id: revoked 700
+id: revoked 701
+id: revoked 702
+id: revoked 703
+id: revoked 704
+id: revoked 705
+id: revoked 706
+id: revoked 707
+id: revoked 708
+id: revoked 709
+id: revoked 710
+id: revoked 711
+id: revoked 712
+id: revoked 713
+id: revoked 714
+id: revoked 715
+id: revoked 716
+id: revoked 717
+id: revoked 718
+id: revoked 719
+id: revoked 720
+id: revoked 721
+id: revoked 722
+id: revoked 723
+id: revoked 724
+id: revoked 725
+id: revoked 726
+id: revoked 727
+id: revoked 728
+id: revoked 729
+id: revoked 730
+id: revoked 731
+id: revoked 732
+id: revoked 733
+id: revoked 734
+id: revoked 735
+id: revoked 736
+id: revoked 737
+id: revoked 738
+id: revoked 739
+id: revoked 740
+id: revoked 741
+id: revoked 742
+id: revoked 743
+id: revoked 744
+id: revoked 745
+id: revoked 746
+id: revoked 747
+id: revoked 748
+id: revoked 749
+id: revoked 750
+id: revoked 751
+id: revoked 752
+id: revoked 753
+id: revoked 754
+id: revoked 755
+id: revoked 756
+id: revoked 757
+id: revoked 758
+id: revoked 759
+id: revoked 760
+id: revoked 761
+id: revoked 762
+id: revoked 763
+id: revoked 764
+id: revoked 765
+id: revoked 766
+id: revoked 767
+id: revoked 768
+id: revoked 769
+id: revoked 770
+id: revoked 771
+id: revoked 772
+id: revoked 773
+id: revoked 774
+id: revoked 775
+id: revoked 776
+id: revoked 777
+id: revoked 778
+id: revoked 779
+id: revoked 780
+id: revoked 781
+id: revoked 782
+id: revoked 783
+id: revoked 784
+id: revoked 785
+id: revoked 786
+id: revoked 787
+id: revoked 788
+id: revoked 789
+id: revoked 790
+id: revoked 791
+id: revoked 792
+id: revoked 793
+id: revoked 794
+id: revoked 795
+id: revoked 796
+id: revoked 797
+id: revoked 798
+id: revoked 799
+id: revoked 999
+id: revoked 1000
+id: revoked 1001
+id: revoked 1002
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-serials b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-serials
new file mode 100644
index 0000000..b20fec2
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-serials
@@ -0,0 +1,19 @@
+serial: 1-4
+serial: 10
+serial: 15
+serial: 30
+serial: 50
+serial: 90
+serial: 999
+# The following sum to 500-799
+serial: 500
+serial: 501
+serial: 502
+serial: 503-600
+serial: 700-797
+serial: 798
+serial: 799
+serial: 599-701
+# Some multiple consecutive serial number ranges
+serial: 10000-20000
+serial: 30000-40000
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha1 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha1
new file mode 100644
index 0000000..475e90c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha1
@@ -0,0 +1,11 @@
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha256 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha256
new file mode 100644
index 0000000..13109e9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha256
@@ -0,0 +1,11 @@
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005
new file mode 100644
index 0000000..d82a0b5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDqONQediveIXoseoT+MWp9yEdMO7hP7F4fAno6gunyoAAAAIig1MZroNTG
+awAAAAtzc2gtZWQyNTUxOQAAACDqONQediveIXoseoT+MWp9yEdMO7hP7F4fAno6gunyoA
+AAAEBSEPLoX4NVkAchYZEGi7hjd5NoVBWuoxqluCGt/fWrYeo41B52K94heix6hP4xan3I
+R0w7uE/sXh8CejqC6fKgAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005-cert.pub
new file mode 100644
index 0000000..59ea422
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGnzDhP/hp83ipkW8T7f0CIXJuPK7ldbJFKDUrkvn6J1AAAAIOo41B52K94heix6hP4xan3IR0w7uE/sXh8CejqC6fKgAAAAAAAAAAUAAAABAAAACXJldm9rZWQgNQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQO9W58IrK+I0o2us9Hs/QBkrEe1YIgl6PzCMsu/Zu/tdZxGDK5Pxoz7tKzXezS9LPGQfZ3fVdl58PZC1DtxQ5gU= ./tst-keys/unrevoked-0005.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005.pub
new file mode 100644
index 0000000..081ac6c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOo41B52K94heix6hP4xan3IR0w7uE/sXh8CejqC6fKg 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009
new file mode 100644
index 0000000..9479498
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDXQqTeALQCMo64B4EX5abjRvrjVu69Mnxgg2q0SB5/oQAAAIgIqeXLCKnl
+ywAAAAtzc2gtZWQyNTUxOQAAACDXQqTeALQCMo64B4EX5abjRvrjVu69Mnxgg2q0SB5/oQ
+AAAECubGChJGu90ZNiP/zF+tTtr0+l7y8BrTDMQ0m0+cU0qtdCpN4AtAIyjrgHgRflpuNG
++uNW7r0yfGCDarRIHn+hAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009-cert.pub
new file mode 100644
index 0000000..9ee8890
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIERRY0M1bHm2Qjyo105OCHWp0UCRHLP0xkMuHnkMDP5eAAAAINdCpN4AtAIyjrgHgRflpuNG+uNW7r0yfGCDarRIHn+hAAAAAAAAAAkAAAABAAAACXJldm9rZWQgOQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQFsA4xJHRCXSyq6GHkKdemfbg+jvUZxHlu/UBoZf4esEHAtx0mXiajbUwkWzkh1vCtxZNZhiLIhxqDcNMu+O+wo= ./tst-keys/unrevoked-0009.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009.pub
new file mode 100644
index 0000000..74a797b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINdCpN4AtAIyjrgHgRflpuNG+uNW7r0yfGCDarRIHn+h 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014
new file mode 100644
index 0000000..6fa4fd9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDvTTMHyjozzZabuUzy61XOKBm4klUjUGSWYtX6T4XtEwAAAIhyFdxYchXc
+WAAAAAtzc2gtZWQyNTUxOQAAACDvTTMHyjozzZabuUzy61XOKBm4klUjUGSWYtX6T4XtEw
+AAAEBtC+f4bz1/qtq5K2Rf+0bPeY3P0OWdD3rvrlGPh8wN5u9NMwfKOjPNlpu5TPLrVc4o
+GbiSVSNQZJZi1fpPhe0TAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014-cert.pub
new file mode 100644
index 0000000..bb954f9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIPes2n/Xk4mm4OpuvHDqx9+76vm+SmFgc9d7ATGT1+C8AAAAIO9NMwfKOjPNlpu5TPLrVc4oGbiSVSNQZJZi1fpPhe0TAAAAAAAAAA4AAAABAAAACnJldm9rZWQgMTQAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDGVORypw3DoMuWBu0V4cH/OgRBstD5cY37CfLrVZpmGv9jDRXVNQee7vYowk0r3XvQPoUecQBIMZGAQtEiw18E ./tst-keys/unrevoked-0014.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014.pub
new file mode 100644
index 0000000..4a866e4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO9NMwfKOjPNlpu5TPLrVc4oGbiSVSNQZJZi1fpPhe0T 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016
new file mode 100644
index 0000000..62d5027
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBWKMDlSwSGo4dcBAZmL+Xxk64Wp/ZfFSu2vkp82JXQCQAAAIjUcNt51HDb
+eQAAAAtzc2gtZWQyNTUxOQAAACBWKMDlSwSGo4dcBAZmL+Xxk64Wp/ZfFSu2vkp82JXQCQ
+AAAEC1V7PD5tJSOUZtpfqVfWyiSIMJkCDFZzTmFs7GBpJE71YowOVLBIajh1wEBmYv5fGT
+rhan9l8VK7a+SnzYldAJAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016-cert.pub
new file mode 100644
index 0000000..367e4ab
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICGqa0xwr0etbKquuBy5/hYQ/rbMrKfEE6XShgb4YWpUAAAAIFYowOVLBIajh1wEBmYv5fGTrhan9l8VK7a+SnzYldAJAAAAAAAAABAAAAABAAAACnJldm9rZWQgMTYAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBKVetE3dsch2wjMIHGoiH8zp6gFMn1KgGKn01EPc1A08a/JKNvaSDYhlARLjiBzjIUGlykhHTTr4EcHTPWl58P ./tst-keys/unrevoked-0016.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016.pub
new file mode 100644
index 0000000..47cac1e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYowOVLBIajh1wEBmYv5fGTrhan9l8VK7a+SnzYldAJ 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029
new file mode 100644
index 0000000..589daa6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA3B1NQ9RFEkJUGcIUcCL22yMVEeob8/PUsk9lYH43vPwAAAIjxPrzV8T68
+1QAAAAtzc2gtZWQyNTUxOQAAACA3B1NQ9RFEkJUGcIUcCL22yMVEeob8/PUsk9lYH43vPw
+AAAED89ht9KdlYRfsKwh+pzh6BOvPf/U58QBkw1d3LfKnn+jcHU1D1EUSQlQZwhRwIvbbI
+xUR6hvz89SyT2Vgfje8/AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029-cert.pub
new file mode 100644
index 0000000..1bf3883
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIEVLRuchC4z7/EqITmyqCxOyhC7/enmFWsalP8FFFYiXAAAAIDcHU1D1EUSQlQZwhRwIvbbIxUR6hvz89SyT2Vgfje8/AAAAAAAAAB0AAAABAAAACnJldm9rZWQgMjkAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEChRFz/Zb6b3znoIWJjd8OTmCIEH7YE/fKWtyWHoGjz02G4VnCfwuHp23yD+k1XsoOGC7xcSnQeqZ19160HDNgC ./tst-keys/unrevoked-0029.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029.pub
new file mode 100644
index 0000000..4072d92
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDcHU1D1EUSQlQZwhRwIvbbIxUR6hvz89SyT2Vgfje8/ 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049
new file mode 100644
index 0000000..b5788a0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACD2mB5GBuavtb/bX7W54OmUCCJzUWBwG7cQ4q/jon1MBQAAAIjRkEU40ZBF
+OAAAAAtzc2gtZWQyNTUxOQAAACD2mB5GBuavtb/bX7W54OmUCCJzUWBwG7cQ4q/jon1MBQ
+AAAECuUtJb+T0um2mGvjD/ZZpbtjIhWc3jGVbzuDnEovOjnPaYHkYG5q+1v9tftbng6ZQI
+InNRYHAbtxDir+OifUwFAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049-cert.pub
new file mode 100644
index 0000000..587cf62
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILZPLEL5xQ8HDLa8pJhchJ3EEhZcjMqACCAEeL+U6c/QAAAAIPaYHkYG5q+1v9tftbng6ZQIInNRYHAbtxDir+OifUwFAAAAAAAAADEAAAABAAAACnJldm9rZWQgNDkAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEB2GglzoC1VgsYNAVd5BDsLbeR5M5hHcVVvNsGnK1QCXMj56cgfkbXLj6W6tjJEEFY4G+KPJh1F/SGJi02P5lkJ ./tst-keys/unrevoked-0049.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049.pub
new file mode 100644
index 0000000..07d5369
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPaYHkYG5q+1v9tftbng6ZQIInNRYHAbtxDir+OifUwF 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051
new file mode 100644
index 0000000..52d3283
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACD39ygfAlHPhZWU8inWu1hypIlQTChQxSKKB6iaV6Q0lQAAAIgMawsqDGsL
+KgAAAAtzc2gtZWQyNTUxOQAAACD39ygfAlHPhZWU8inWu1hypIlQTChQxSKKB6iaV6Q0lQ
+AAAEB4Ng9MekhsMKYDaBcOUWdxmi1rjgCsPOOfpABTxiCef/f3KB8CUc+FlZTyKda7WHKk
+iVBMKFDFIooHqJpXpDSVAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051-cert.pub
new file mode 100644
index 0000000..5b4bd11
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGTNYRrlJ1vExK7dume319Krn4YW6wyZc4PzZLjZoB8zAAAAIPf3KB8CUc+FlZTyKda7WHKkiVBMKFDFIooHqJpXpDSVAAAAAAAAADMAAAABAAAACnJldm9rZWQgNTEAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEAgUiwWKerMo8nuejTER/EmM6ZUpmXjgFwPCpb1LAxBJH71iOnyF9S0gp+CSmjqiTS2yuQajSMen64wOdJCX7wF ./tst-keys/unrevoked-0051.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051.pub
new file mode 100644
index 0000000..88867e5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPf3KB8CUc+FlZTyKda7WHKkiVBMKFDFIooHqJpXpDSV 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499
new file mode 100644
index 0000000..8f59be9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCpwI1aCbAOVvA7NJhLtBNpR4tiGGtTQ019wjKL6zJ/uQAAAIhllrzrZZa8
+6wAAAAtzc2gtZWQyNTUxOQAAACCpwI1aCbAOVvA7NJhLtBNpR4tiGGtTQ019wjKL6zJ/uQ
+AAAECQ6o+3J9W3wXFWEcrPJl5qJZudUPmPdKF7SYxcMTrVP6nAjVoJsA5W8Ds0mEu0E2lH
+i2IYa1NDTX3CMovrMn+5AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499-cert.pub
new file mode 100644
index 0000000..a6e76f1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIJvt1IxZsGIIS9DDCCKiD13Dbs5Af5ouews+YwZ9FoydAAAAIKnAjVoJsA5W8Ds0mEu0E2lHi2IYa1NDTX3CMovrMn+5AAAAAAAAAfMAAAABAAAAC3Jldm9rZWQgNDk5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAMaA4UjND4LX9kdHjhgWJjGzzs/xUBwxQQcAmNgwmmQzmkwj8ctWBBA1+TkBMcZbSNUWBdclT4UcnDPEYqG1NBg== ./tst-keys/unrevoked-0499.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499.pub
new file mode 100644
index 0000000..5a3acbb
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKnAjVoJsA5W8Ds0mEu0E2lHi2IYa1NDTX3CMovrMn+5 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800
new file mode 100644
index 0000000..9684d72
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAn5h8A2vYJ1+IWVtdLMulUQKCqlVLHpcHEFqYC5gtGlwAAAIh2lf7UdpX+
+1AAAAAtzc2gtZWQyNTUxOQAAACAn5h8A2vYJ1+IWVtdLMulUQKCqlVLHpcHEFqYC5gtGlw
+AAAEAEXGgMPKs3HwkQmNdVkbO3PcaBVCBEv1l8yy/ly30jPSfmHwDa9gnX4hZW10sy6VRA
+oKqVUselwcQWpgLmC0aXAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800-cert.pub
new file mode 100644
index 0000000..ab47a2b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIPAKFTJ25v9CsCppsQ/FwXAZgntAIdQHUXo0KQ3FrlTzAAAAICfmHwDa9gnX4hZW10sy6VRAoKqVUselwcQWpgLmC0aXAAAAAAAAAyAAAAABAAAAC3Jldm9rZWQgODAwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABA16aKfsgD0iZ+qc2b1AxBHZ/nyczN2Xjbhg4eJm/6cPSkBHs8uan5e8yPBIQJq2LztC3If6Z6PARoWUnIKb43CQ== ./tst-keys/unrevoked-0800.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800.pub
new file mode 100644
index 0000000..3a41f29
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfmHwDa9gnX4hZW10sy6VRAoKqVUselwcQWpgLmC0aX 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010
new file mode 100644
index 0000000..89df717
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAg0jawQzRMO/ESfFm6yDc66J5kjasOqTb7rmQSU6Nk3QAAAIhczXMoXM1z
+KAAAAAtzc2gtZWQyNTUxOQAAACAg0jawQzRMO/ESfFm6yDc66J5kjasOqTb7rmQSU6Nk3Q
+AAAEAdeQiqpyZqBaffmgy+UrvFVpygD0n8isn3zjumVNtKxiDSNrBDNEw78RJ8WbrINzro
+nmSNqw6pNvuuZBJTo2TdAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010-cert.pub
new file mode 100644
index 0000000..2d0fe53
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIITg9nSjjofIKXTKf2byvYL3Ce43PP9Dtrbj/+AlfgEtAAAAICDSNrBDNEw78RJ8WbrINzronmSNqw6pNvuuZBJTo2TdAAAAAAAAA/IAAAABAAAADHJldm9rZWQgMTAxMAAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQIndHhKILtU0+FkKKw1KmhaHQS3p1KiQdld/2P5jpcEgb292iY+ICU+aHXKvS8qGM2aMImv8835NEyWy/MB74QM= ./tst-keys/unrevoked-1010.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010.pub
new file mode 100644
index 0000000..05c5eac
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICDSNrBDNEw78RJ8WbrINzronmSNqw6pNvuuZBJTo2Td 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011
new file mode 100644
index 0000000..38b8232
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCd4IBQx9BhO9FzYMOKu3cKgBcwUwb7XzS3uI26RgmEYgAAAIjHvhtux74b
+bgAAAAtzc2gtZWQyNTUxOQAAACCd4IBQx9BhO9FzYMOKu3cKgBcwUwb7XzS3uI26RgmEYg
+AAAEBsteyDUYUNwgY3SMkMs0guy8MJfek2kuvH35zEpVf6Hp3ggFDH0GE70XNgw4q7dwqA
+FzBTBvtfNLe4jbpGCYRiAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011-cert.pub
new file mode 100644
index 0000000..4671638
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIMjD2+xjmUC1VviOH+peT9C81Y4xjyTue/F69nFKmQBMAAAAIJ3ggFDH0GE70XNgw4q7dwqAFzBTBvtfNLe4jbpGCYRiAAAAAAAAA/MAAAABAAAADHJldm9rZWQgMTAxMQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQNENdVFCE02X6z+wFJtm2DQcgdc4oov9DyFKLPqLrogo+pVao5QwOkeJ2J/tmp40H2+uP/jrDlQuCvOcoQGHqwY= ./tst-keys/unrevoked-1011.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011.pub
new file mode 100644
index 0000000..0809077
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ3ggFDH0GE70XNgw4q7dwqAFzBTBvtfNLe4jbpGCYRi 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key
new file mode 100644
index 0000000..ee3f922
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiwAAAJDdMhQO3TIU
+DgAAAAtzc2gtZWQyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiw
+AAAEA4NlTFs3h2zqt5pSZ5S3dJb42GE7EjG16coKj70eELNDeK9TvB/fuMTUhMLkpCNV5W
+XIVOSBYnG5vy3fzSi+OLAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub
new file mode 100644
index 0000000..2be08be
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGXo4+L/NyBl1VQDP39PxJP3LSzaqopqZGVP3cG0WoFAAAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OLAAAAAAAAAAUAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEA/HwKB8J/kvkEsdxDou+UebnR9u30xPH6FEnbHLlfKbKMIXwLFIHnf9F6bTL36WhFDEDcSBGS19VBWBDRosM8L
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub
new file mode 100644
index 0000000..0255005
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OL
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle
new file mode 100644
index 0000000..c402f54
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key
new file mode 100644
index 0000000..3dd37be
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czgAAAJDhSMqA4UjK
+gAAAAAtzc2gtZWQyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czg
+AAAEB1yC00NMYEAVzhDj9odGVL0EonaIkf5jdUZ/czJ0+SPWATOZ8PcOKdY978fzIstnZ0
++FuefIWKp7wRZynQLdzOAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub
new file mode 100644
index 0000000..de191d1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFEmoWkYraMju0JI0b/0RQtR6RYo/OVp53EVf48L/Pu/AAAAIBmHlkHFlA7HkoTZcau80PH5zduQu41m8BqnH/1v2BwVAAAAAAAAAAEAAAABAAAACGFfa2V5X2lkAAAAFgAAABJ0ZXN0ZXJAZXhhbXBsZS5jb20AAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg2ifM9NMuXwQf7H/H5LCMhMjVqugyyN+jmcMoJUL2YLAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQG1kXUido46YOnmwvkJuIAKyp6Q9Gr+lbdOQvU0St/Hc9HTTIxgDGyLpv0alIJpHOuSYUUUxDufvGKtLJK1duwg= ./signing_key.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub
new file mode 100644
index 0000000..e1210e7
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java
new file mode 100644
index 0000000..fdfffce
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneOffset;
+
+import org.eclipse.jgit.api.CommitCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Common setup for SSH signature tests.
+ */
+public abstract class AbstractSshSignatureTest extends RepositoryTestCase {
+
+	@Rule
+	public TemporaryFolder keys = new TemporaryFolder();
+
+	protected File certs;
+
+	protected Instant commitTime;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		copyResource("allowed_signers", keys.getRoot());
+		copyResource("other_key", keys.getRoot());
+		copyResource("other_key.pub", keys.getRoot());
+		copyResource("other_key-cert.pub", keys.getRoot());
+		copyResource("signing_key", keys.getRoot());
+		copyResource("signing_key.pub", keys.getRoot());
+		certs = keys.newFolder("certs");
+		copyResource("certs/expired.cert", certs);
+		copyResource("certs/no_principals.cert", certs);
+		copyResource("certs/other.cert", certs);
+		copyResource("certs/other-ca.cert", certs);
+		copyResource("certs/tester.cert", certs);
+		copyResource("certs/two_principals.cert", certs);
+		Repository repo = db;
+		StoredConfig config = repo.getConfig();
+		config.setString("gpg", null, "format", "ssh");
+		config.setString("gpg", "ssh", "allowedSignersFile",
+				keys.getRoot().toPath().resolve("allowed_signers").toString()
+						.replace('\\', '/'));
+		config.save();
+		// Run all tests with commit times on 2024-10-02T12:00:00Z. The test
+		// certificates are valid from 2024-09-01 to 2024-10-31, except the
+		// "expired" certificate which is valid only on 2024-09-01.
+		commitTime = Instant.parse("2024-10-02T12:00:00.00Z");
+	}
+
+	private void copyResource(String name, File directory) throws IOException {
+		try (InputStream in = this.getClass().getResourceAsStream(name)) {
+			int i = name.lastIndexOf('/');
+			String fileName = i < 0 ? name : name.substring(i + 1);
+			Files.copy(in, directory.toPath().resolve(fileName));
+		}
+	}
+
+	protected RevCommit createSignedCommit(String certificate,
+			String signingKey) throws Exception {
+		Repository repo = db;
+		Path key = keys.getRoot().toPath().resolve(signingKey);
+		if (certificate != null) {
+			Files.copy(certs.toPath().resolve(certificate),
+					keys.getRoot().toPath().resolve(signingKey),
+					StandardCopyOption.REPLACE_EXISTING);
+		}
+		PersonIdent commitAuthor = new PersonIdent("tester",
+				"tester@example.com", commitTime, ZoneOffset.UTC);
+		try (Git git = Git.wrap(repo)) {
+			writeTrashFile("foo.txt", "foo");
+			git.add().addFilepattern("foo.txt").call();
+			CommitCommand commit = git.commit();
+			commit.setAuthor(commitAuthor);
+			commit.setCommitter(commitAuthor);
+			commit.setMessage("Message");
+			commit.setSign(Boolean.TRUE);
+			commit.setSigningKey(key.toAbsolutePath().toString());
+			return commit.call();
+		}
+	}
+
+	protected RevCommit checkSshSignature(RevCommit c) {
+		byte[] sig = c.getRawGpgSignature();
+		assertNotNull(sig);
+		String signature = new String(sig, StandardCharsets.US_ASCII);
+		assertTrue("Not an SSH signature:\n" + signature,
+				signature.startsWith(Constants.SSH_SIGNATURE_PREFIX));
+		return c;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AllowedSignersParseTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AllowedSignersParseTest.java
new file mode 100644
index 0000000..90fde3f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AllowedSignersParseTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import java.io.StreamCorruptedException;
+import java.time.Instant;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the line parsing in {@link AllowedSigners}.
+ */
+public class AllowedSignersParseTest {
+
+	@Before
+	public void setup() {
+		// Uses GMT-03:30 as time zone.
+		SystemReader.setInstance(new MockSystemReader());
+	}
+
+	@After
+	public void tearDown() {
+		SystemReader.setInstance(null);
+	}
+
+	@Test
+	public void testValidDate() {
+		assertEquals(Instant.parse("2024-09-01T00:00:00.00Z"),
+				AllowedSigners.parseDate("20240901Z"));
+		assertEquals(Instant.parse("2024-09-01T01:02:00.00Z"),
+				AllowedSigners.parseDate("202409010102Z"));
+		assertEquals(Instant.parse("2024-09-01T01:02:03.00Z"),
+				AllowedSigners.parseDate("20240901010203Z"));
+		assertEquals(Instant.parse("2024-09-01T03:30:00.00Z"),
+				AllowedSigners.parseDate("20240901"));
+		assertEquals(Instant.parse("2024-09-01T04:32:00.00Z"),
+				AllowedSigners.parseDate("202409010102"));
+		assertEquals(Instant.parse("2024-09-01T04:32:03.00Z"),
+				AllowedSigners.parseDate("20240901010203"));
+	}
+
+	@Test
+	public void testInvalidDate() {
+		assertThrows(Exception.class, () -> AllowedSigners.parseDate("1234"));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parseDate("09/01/2024"));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parseDate("2024-09-01"));
+	}
+
+	private void checkValidKey(String expected, String input, int from)
+			throws StreamCorruptedException {
+		assertEquals(expected, AllowedSigners.parsePublicKey(input, from));
+	}
+	@Test
+	public void testValidPublicKey() throws StreamCorruptedException {
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				0);
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"xyzssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				3);
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"xyz ssh-ed25519   AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO abc",
+				3);
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"xyz\tssh-ed25519 \tAAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO abc",
+				3);
+	}
+
+	@Test
+	public void testInvalidPublicKey() {
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey(null, 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("", 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("foo", 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", -1));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", 12));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", 13));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", 16));
+	}
+
+	@Test
+	public void testValidDequote() {
+		assertEquals(new AllowedSigners.Dequoted("a\\bc", 4),
+				AllowedSigners.dequote("a\\bc", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\bc\"", 5),
+				AllowedSigners.dequote("a\\bc\"", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\b\"c", 5),
+				AllowedSigners.dequote("a\\b\"c", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\b\"c", 8),
+				AllowedSigners.dequote("\"a\\b\\\"c\"", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\b\"c", 11),
+				AllowedSigners.dequote("xyz\"a\\b\\\"c\"", 3));
+		assertEquals(new AllowedSigners.Dequoted("abc", 6),
+				AllowedSigners.dequote("   abc def", 3));
+	}
+
+	@Test
+	public void testInvalidDequote() {
+		assertThrows(Exception.class, () -> AllowedSigners.dequote("\"abc", 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.dequote("\"abc\\\"", 0));
+	}
+
+	@Test
+	public void testValidLine() throws Exception {
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "*@a.com" },
+				true, null, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"*@a.com cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "*@a.com", "*@b.a.com" },
+				true, null, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"*@a.com,*@b.a.com cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, null, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"foo@a.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, new String[] { "foo", "bar" }, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"foo@a.com namespaces=\"foo,bar\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, null, Instant.parse("2024-09-01T03:30:00.00Z"), null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"foo@a.com valid-After=\"20240901\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "*@a.com", "*@b.a.com" },
+				true, new String[] { "git" },
+				Instant.parse("2024-09-01T03:30:00.00Z"),
+				Instant.parse("2024-09-01T12:00:00.00Z"),
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"*@a.com,*@b.a.com cert-authority namespaces=\"git\" valid-after=\"20240901\" valid-before=\"202409011200Z\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+	}
+
+	@Test
+	public void testInvalidLine() {
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"namespaces=\"git\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"valid-after=\"20240901\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"valid-before=\"20240901\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"a@a.com namespaces=\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"a@a.com namespaces=\",,,\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"a@a.com,,b@a.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+	}
+
+	@Test
+	public void testSkippedLine() throws Exception {
+		assertNull(AllowedSigners.parseLine(null));
+		assertNull(AllowedSigners.parseLine(""));
+		assertNull(AllowedSigners.parseLine("# Comment"));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrlLoadTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrlLoadTest.java
new file mode 100644
index 0000000..9f9c3ca
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrlLoadTest.java
@@ -0,0 +1,32 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+
+import org.junit.Test;
+
+/**
+ * Tests loading an {@link OpenSshBinaryKrl}.
+ */
+public class OpenSshBinaryKrlLoadTest {
+
+	@Test
+	public void testLoad() throws Exception {
+		try (InputStream in = new BufferedInputStream(
+				this.getClass().getResourceAsStream("krl/krl"))) {
+			OpenSshBinaryKrl krl = OpenSshBinaryKrl.load(in, false);
+			assertNotNull(krl);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshKrlTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshKrlTest.java
new file mode 100644
index 0000000..2fd7756
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshKrlTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for {@link OpenSshKrl} using binary KRLs.
+ */
+@RunWith(Parameterized.class)
+public class OpenSshKrlTest {
+
+	// The test data was generated using the public domain OpenSSH test script
+	// with some minor modifications (une ed25519 always, generate a "includes
+	// everything KRl, name the unrekoked keys "unrevoked*", generate a plain
+	// text KRL, and don't run the ssh-keygen tests). The original script is
+	// available at
+	// https://github.com/openssh/openssh-portable/blob/67a115e/regress/krl.sh
+
+	private static final String[] KRLS = {
+			"krl-empty", "krl-keys", "krl-all",
+			"krl-sha1", "krl-sha256", "krl-hash",
+			"krl-serial", "krl-keyid", "krl-cert", "krl-ca",
+			"krl-serial-wild", "krl-keyid-wild", "krl-text" };
+
+	private static final int[] REVOKED = { 1, 4, 10, 50, 90, 500, 510, 520, 550,
+			799, 999 };
+
+	private static final int[] UNREVOKED = { 5, 9, 14, 16, 29, 49, 51, 499, 800,
+			1010, 1011 };
+
+	private static class TestData {
+
+		String key;
+
+		String krl;
+
+		Boolean expected;
+
+		TestData(String key, String krl, boolean expected) {
+			this.key = key;
+			this.krl = krl;
+			this.expected = Boolean.valueOf(expected);
+		}
+
+		@Override
+		public String toString() {
+			return key + '-' + krl;
+		}
+	}
+
+	@Parameters(name = "{0}")
+	public static List<TestData> initTestData() {
+		List<TestData> tests = new ArrayList<>();
+		for (int i = 0; i < REVOKED.length; i++) {
+			String key = String.format("revoked-%04d",
+					Integer.valueOf(REVOKED[i]));
+			for (String krl : KRLS) {
+				boolean expected = !krl.endsWith("-empty");
+				tests.add(new TestData(key + "-cert.pub", krl, expected));
+				expected = krl.endsWith("-keys") || krl.endsWith("-all")
+						|| krl.endsWith("-hash") || krl.endsWith("-sha1")
+						|| krl.endsWith("-sha256") || krl.endsWith("-text");
+				tests.add(new TestData(key + ".pub", krl, expected));
+			}
+		}
+		for (int i = 0; i < UNREVOKED.length; i++) {
+			String key = String.format("unrevoked-%04d",
+					Integer.valueOf(UNREVOKED[i]));
+			for (String krl : KRLS) {
+				boolean expected = false;
+				tests.add(new TestData(key + ".pub", krl, expected));
+				expected = krl.endsWith("-ca");
+				tests.add(new TestData(key + "-cert.pub", krl, expected));
+			}
+		}
+		return tests;
+	}
+
+	private static Path tmp;
+
+	@BeforeClass
+	public static void setUp() throws IOException {
+		tmp = Files.createTempDirectory("krls");
+		for (String krl : KRLS) {
+			copyResource("krl/" + krl, tmp);
+		}
+	}
+
+	private static void copyResource(String name, Path directory)
+			throws IOException {
+		try (InputStream in = OpenSshKrlTest.class
+				.getResourceAsStream(name)) {
+			int i = name.lastIndexOf('/');
+			String fileName = i < 0 ? name : name.substring(i + 1);
+			Files.copy(in, directory.resolve(fileName));
+		}
+	}
+
+	@AfterClass
+	public static void cleanUp() throws Exception {
+		FileUtils.delete(tmp.toFile(), FileUtils.RECURSIVE);
+	}
+
+	// Injected by JUnit
+	@Parameter
+	public TestData data;
+
+	@Test
+	public void testIsRevoked() throws Exception {
+		OpenSshKrl krl = new OpenSshKrl(tmp.resolve(data.krl));
+		try (InputStream in = this.getClass()
+				.getResourceAsStream("krl/" + data.key)) {
+			PublicKey key = AuthorizedKeyEntry.readAuthorizedKeys(in, true)
+					.get(0)
+					.resolvePublicKey(null, PublicKeyEntryResolver.FAILING);
+			assertEquals(data.expected, Boolean.valueOf(krl.isRevoked(key)));
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SerialRangeSetTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SerialRangeSetTest.java
new file mode 100644
index 0000000..e6709ad
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SerialRangeSetTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Tests for the set of serial number ranges.
+ */
+public class SerialRangeSetTest {
+
+	private SerialRangeSet ranges = new SerialRangeSet();
+
+	@Test
+	public void testInsertSimple() {
+		ranges.add(1);
+		ranges.add(3);
+		ranges.add(5);
+		assertEquals(3, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertFalse(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertFalse(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertFalse(ranges.contains(6));
+	}
+
+	@Test
+	public void testInsertSimpleRanges() {
+		ranges.add(1, 2);
+		ranges.add(4, 5);
+		ranges.add(7, 8);
+		assertEquals(3, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertFalse(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertFalse(ranges.contains(6));
+		assertTrue(ranges.contains(7));
+		assertTrue(ranges.contains(8));
+		assertFalse(ranges.contains(9));
+	}
+
+	@Test
+	public void testInsertCoalesce() {
+		ranges.add(5);
+		ranges.add(1);
+		ranges.add(2);
+		ranges.add(4);
+		ranges.add(7);
+		ranges.add(3);
+		assertEquals(2, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertFalse(ranges.contains(6));
+		assertTrue(ranges.contains(7));
+		assertFalse(ranges.contains(8));
+	}
+
+	@Test
+	public void testInsertOverlap() {
+		ranges.add(1, 3);
+		ranges.add(6);
+		ranges.add(2, 5);
+		assertEquals(1, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertTrue(ranges.contains(6));
+		assertFalse(ranges.contains(7));
+	}
+
+	@Test
+	public void testInsertOverlapMultiple() {
+		ranges.add(1, 3);
+		ranges.add(5, 6);
+		ranges.add(8);
+		ranges.add(2, 5);
+		assertEquals(2, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertTrue(ranges.contains(6));
+		assertFalse(ranges.contains(7));
+		assertTrue(ranges.contains(8));
+		assertFalse(ranges.contains(9));
+	}
+
+	@Test
+	public void testInsertOverlapTotal() {
+		ranges.add(1, 3);
+		ranges.add(2, 3);
+		assertEquals(1, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertFalse(ranges.contains(4));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java
new file mode 100644
index 0000000..79ca21f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.InputStream;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link SshCertificateUtils}. They use a certificate valid from
+ * 2024-09-01 00:00:00 to 2024-09-02 00:00:00 UTC.
+ */
+public class SshCertificateUtilsTest {
+
+	private OpenSshCertificate certificate;
+
+	@Before
+	public void loadCertificate() throws Exception {
+		try (InputStream in = this.getClass().getResourceAsStream(
+				"certs/expired.cert")) {
+			List<AuthorizedKeyEntry> keys = AuthorizedKeyEntry
+					.readAuthorizedKeys(in, true);
+			if (keys.isEmpty()) {
+				certificate = null;
+			}
+			PublicKey key = keys.get(0).resolvePublicKey(null,
+					PublicKeyEntryResolver.FAILING);
+			assertTrue(
+					"Expected an OpenSshKeyCertificate but got a "
+							+ key.getClass().getName(),
+					key instanceof OpenSshCertificate);
+			certificate = (OpenSshCertificate) key;
+		}
+	}
+
+	@Test
+	public void testValidUserCertificate() {
+		assertNull(SshCertificateUtils.verify(certificate, null));
+		Instant validTime = Instant.parse("2024-09-01T00:00:00.00Z");
+		assertNull(SshCertificateUtils.verify(certificate, validTime));
+		assertNull(SshCertificateUtils.checkExpiration(certificate, validTime));
+	}
+
+	@Test
+	public void testCheckTooEarly() {
+		Instant invalidTime = Instant.parse("2024-08-31T23:59:59.00Z");
+		assertNotNull(
+				SshCertificateUtils.checkExpiration(certificate, invalidTime));
+		assertNotNull(SshCertificateUtils.verify(certificate, invalidTime));
+	}
+
+	@Test
+	public void testCheckExpired() {
+		Instant invalidTime = Instant.parse("2024-09-02T00:00:01.00Z");
+		assertNotNull(
+				SshCertificateUtils.checkExpiration(certificate, invalidTime));
+		assertNotNull(SshCertificateUtils.verify(certificate, invalidTime));
+	}
+
+	@Test
+	public void testInvalidSignature() throws Exception {
+		// Modify the serialized certificate, then re-load it again. To check that
+		// serialization per se works fine, also check an unmodified version.
+		Buffer buffer = new ByteArrayBuffer();
+		buffer.putPublicKey(certificate);
+		int pos = buffer.rpos();
+		PublicKey unchanged = buffer.getPublicKey();
+		assertTrue(
+				"Expected an OpenSshCertificate but got a "
+						+ unchanged.getClass().getName(),
+				unchanged instanceof OpenSshCertificate);
+		assertNull(SshCertificateUtils.verify((OpenSshCertificate) unchanged,
+				null));
+		buffer.rpos(pos);
+		// Change a byte. The test certificate has the key ID at offset 128.
+		// Changing a byte in the key ID should still result in a successful
+		// deserialization, but then fail the signature check.
+		buffer.array()[pos + 128]++;
+		PublicKey changed = buffer.getPublicKey();
+		assertTrue(
+				"Expected an OpenSshCertificate but got a "
+						+ changed.getClass().getName(),
+				changed instanceof OpenSshCertificate);
+		assertNotNull(
+				SshCertificateUtils.verify((OpenSshCertificate) changed, null));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifierTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifierTest.java
new file mode 100644
index 0000000..e5dfe49
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifierTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.VerificationResult;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.StringUtils;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link SshSignatureVerifier}.
+ */
+public class SshSignatureVerifierTest extends AbstractSshSignatureTest {
+
+	@Test
+	public void testPlainSignature() throws Exception {
+		RevCommit c = checkSshSignature(
+				createSignedCommit(null, "signing_key.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("tester@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testCertificateSignature() throws Exception {
+		RevCommit c = checkSshSignature(
+				createSignedCommit("tester.cert", "signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("tester@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testNoPrincipalsSignature() throws Exception {
+		RevCommit c = checkSshSignature(createSignedCommit("no_principals.cert",
+				"signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertFalse(v.verified());
+			assertFalse(v.expired());
+			assertNull(v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.NEVER, v.trustLevel());
+			assertTrue(v.message().contains("*@example.com"));
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testOtherCertificateSignature() throws Exception {
+		RevCommit c = checkSshSignature(
+				createSignedCommit("other.cert", "signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("other@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testTwoPrincipalsCertificateSignature() throws Exception {
+		RevCommit c = checkSshSignature(createSignedCommit(
+				"two_principals.cert", "signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("foo@example.com,tester@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java
new file mode 100644
index 0000000..b3a4482
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+/**
+ * Tests for the {@link SshSigner}.
+ */
+public class SshSignerTest extends AbstractSshSignatureTest {
+
+	@Test
+	public void testPlainSignature() throws Exception {
+		checkSshSignature(createSignedCommit(null, "signing_key.pub"));
+	}
+
+	@Test
+	public void testExpiredSignature() throws Exception {
+		Throwable t = assertThrows(Throwable.class,
+				() -> createSignedCommit("expired.cert",
+						"signing_key-cert.pub"));
+		// The exception or one of its causes should mention "[Ee]xpired" and
+		// "[Cc]ertificate" in the message
+		while (t != null) {
+			String message = t.getMessage();
+			if (message.contains("xpired") && message.contains("ertificate")) {
+				return;
+			}
+			t = t.getCause();
+		}
+		fail("Expected exception message not found");
+	}
+
+	@Test
+	public void testCertificateSignature() throws Exception {
+		checkSshSignature(createSignedCommit("tester.cert", "signing_key.pub"));
+	}
+
+	@Test
+	public void testNoPrincipalsSignature() throws Exception {
+		// Certificate has no principals; should still work
+		checkSshSignature(
+				createSignedCommit("no_principals.cert", "signing_key.pub"));
+	}
+
+	@Test
+	public void testOtherSignature() throws Exception {
+		// Certificate has a principal different that tester@example.com; should
+		// still work
+		checkSshSignature(createSignedCommit("other.cert", "signing_key.pub"));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/VerifyGitSignaturesTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/VerifyGitSignaturesTest.java
new file mode 100644
index 0000000..30ddee5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/VerifyGitSignaturesTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.internal.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.VerificationResult;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.SignatureUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Verifies signatures made with C git and OpenSSH 9.0 to ensure we arrive at
+ * the same good/bad decisions, and that we can verify signatures not created by
+ * ourselves.
+ * <p>
+ * Clones a JGit repo from a git bundle file created with C git, then checks all
+ * the commits and their signatures. (All commits in that bundle have SSH
+ * signatures.)
+ * </p>
+ */
+public class VerifyGitSignaturesTest extends LocalDiskRepositoryTestCase {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(VerifyGitSignaturesTest.class);
+
+	@Rule
+	public TemporaryFolder bundleDir = new TemporaryFolder();
+
+	@Before
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		try (InputStream in = this.getClass()
+				.getResourceAsStream("repo.bundle")) {
+			Files.copy(in, bundleDir.getRoot().toPath().resolve("repo.bundle"));
+		}
+		try (InputStream in = this.getClass()
+				.getResourceAsStream("allowed_signers")) {
+			Files.copy(in,
+					bundleDir.getRoot().toPath().resolve("allowed_signers"));
+		}
+	}
+
+	/**
+	 * Tests signatures created by C git using OpenSSH 9.0.
+	 */
+	@Test
+	public void testGitSignatures() throws Exception {
+		File gitDir = new File(getTemporaryDirectory(), "repo.git");
+		try (Git git = Git.cloneRepository().setBare(true)
+				.setGitDir(gitDir)
+				.setURI(new File(bundleDir.getRoot(), "repo.bundle").toURI()
+						.toString())
+				.setBranch("master")
+				.call()) {
+			StoredConfig config = git.getRepository().getConfig();
+			config.setString("gpg", "ssh", "allowedSignersFile",
+					bundleDir.getRoot().toPath().resolve("allowed_signers")
+							.toAbsolutePath().toString().replace('\\', '/'));
+			config.save();
+			List<String> commits = new ArrayList<>();
+			Map<String, PersonIdent> committers = new HashMap<>();
+			git.log().all().call().forEach(c -> {
+				commits.add(c.getName());
+				committers.put(c.getName(), c.getCommitterIdent());
+			});
+			Map<String, Boolean> expected = new HashMap<>();
+			// These two commits do have multiple principals. GIT just reports
+			// the first one; we report both.
+			expected.put("9f79a7b661a22ab1ddf8af880d23678ae7696b71",
+					Boolean.TRUE);
+			expected.put("435108d157440e77d61a914b6a5736bc831c874d",
+					Boolean.TRUE);
+			// This commit has a wrong commit message; the certificate used
+			// did _not_ have two principals, but only a single principal
+			// foo@example.org.
+			expected.put("779dac7de40ebc3886af87d5e6680a09f8b13a3e",
+					Boolean.TRUE);
+			// Signed with other_key-cert.pub: we still don't know the key,
+			// but we do know the certificate's CA key, and trust it, so it's
+			// accepted as a signature from the principal(s) listed in the
+			// certificate.
+			expected.put("951f06d5b5598b721b98d98b04e491f234c1926a",
+					Boolean.TRUE);
+			// Signature with other_key.pub not listed in allowed_signers
+			expected.put("984e629c6d543a7f77eb49a8c9316f2ae4416375",
+					Boolean.FALSE);
+			// Signed with other-ca.cert (CA key not in allowed_signers), but
+			// the certified key _is_ listed in allowed_signers.
+			expected.put("1d7ac6d91747a9c9a777df238fbdaeffa7731a6c",
+					Boolean.FALSE);
+			expected.put("a297bcfbf5c4a850f9770655fef7315328a4b3fb",
+					Boolean.TRUE);
+			expected.put("852729d54676cb83826ed821dc7734013e97950d",
+					Boolean.TRUE);
+			// Signature with a certificate without principals.
+			expected.put("e39a049f75fe127eb74b30aba4b64e171d4281dd",
+					Boolean.FALSE);
+			// Signature made with expired.cert (expired at the commit time).
+			// git/OpenSSH 9.0 allows to create such signatures, but reports
+			// them as FALSE. Our SshSigner doesn't allow creating such
+			// signatures.
+			expected.put("303ea5e61feacdad4cb012b4cb6b0cea3fbcef9f",
+					Boolean.FALSE);
+			expected.put("1ae4b120a869b72a7a2d4ad4d7a8c9d454384333",
+					Boolean.TRUE);
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addNames(commits).call();
+			GitDateFormatter dateFormat = new GitDateFormatter(
+					GitDateFormatter.Format.ISO);
+			for (String oid : commits) {
+				VerificationResult v = results.get(oid);
+				assertNotNull(v);
+				assertNull(v.getException());
+				SignatureVerifier.SignatureVerification sv = v
+						.getVerification();
+				assertNotNull(sv);
+				LOG.info("Commit {}\n{}", oid, SignatureUtils.toString(sv,
+						committers.get(oid), dateFormat));
+				Boolean wanted = expected.get(oid);
+				assertNotNull(wanted);
+				assertEquals(wanted, Boolean.valueOf(sv.verified()));
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index b7ea87f..d12eed0 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -6,9 +6,10 @@
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.internal.transport.sshd;version="7.0.2";x-internal:=true;
+Export-Package: org.eclipse.jgit.internal.signing.ssh;version="7.1.2";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.internal.transport.sshd;version="7.1.2";x-internal:=true;
   uses:="org.apache.sshd.client,
    org.apache.sshd.client.auth,
    org.apache.sshd.client.auth.keyboard,
@@ -23,18 +24,19 @@
    org.apache.sshd.common.signature,
    org.apache.sshd.common.util.buffer,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.internal.transport.sshd.agent;version="7.0.2";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.auth;version="7.0.2";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.pkcs11;version="7.0.2";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.proxy;version="7.0.2";x-friends:="org.eclipse.jgit.ssh.apache.test",
- org.eclipse.jgit.transport.sshd;version="7.0.2";
+ org.eclipse.jgit.internal.transport.sshd.agent;version="7.1.2";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.auth;version="7.1.2";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.pkcs11;version="7.1.2";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="7.1.2";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.signing.ssh;version="7.1.2";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.transport.sshd;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.apache.sshd.client.config.hosts,
    org.apache.sshd.common.keyprovider,
    org.eclipse.jgit.util,
    org.apache.sshd.client.session,
    org.apache.sshd.client.keyverifier",
- org.eclipse.jgit.transport.sshd.agent;version="7.0.2",
+ org.eclipse.jgit.transport.sshd.agent;version="7.1.2",
  sun.security.x509
 Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)",
  org.apache.sshd.agent;version="[2.14.0,2.15.0)",
@@ -89,12 +91,14 @@
  org.apache.sshd.sftp;version="[2.14.0,2.15.0)",
  org.apache.sshd.sftp.client;version="[2.14.0,2.15.0)",
  org.apache.sshd.sftp.common;version="[2.14.0,2.15.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.fnmatch;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.fnmatch;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF
index c328382..1a63852 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.apache - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache/pom.xml b/org.eclipse.jgit.ssh.apache/pom.xml
index c189776..857f8d6 100644
--- a/org.eclipse.jgit.ssh.apache/pom.xml
+++ b/org.eclipse.jgit.ssh.apache/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
diff --git a/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory
new file mode 100644
index 0000000..4a0f553
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.signing.ssh.SshSignatureVerifierFactory
\ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
new file mode 100644
index 0000000..80f22c0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.signing.ssh.SshSignerFactory
\ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
index 7da7181..6048239 100644
--- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
+++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
@@ -125,4 +125,69 @@
 sshCommandTimeout={0} timed out after {1} seconds while opening the channel
 sshProcessStillRunning={0} is not yet completed, cannot get exit code
 sshProxySessionCloseFailed=Error while closing proxy session {0}
+signAllowedSignersCertAuthorityError=Garbage after cert-authority
+signAllowedSignersEmptyIdentity=Identities contains an empty identity; check for spurious extra commas: {0}
+signAllowedSignersEmptyNamespaces=Empty namespaces= is not allowed; to allow a key for any namespace, omit the namespaces option
+signAllowedSignersFormatError=Cannot parse allowed signers file {0}, problem at line {1}: {2}
+signAllowedSignersInvalidDate=Cannot parse valid-before or valid-after date {0}
+signAllowedSignersLineFormat=Invalid line format
+signAllowedSignersMultiple={0} is allowed only once
+signAllowedSignersNoIdentities=Line has no identity patterns
+signAllowedSignersPublicKeyParsing=Cannot parse public key {0}
+signAllowedSignersUnterminatedQuote=Unterminated double quote
+signCertAlgorithmMismatch=Certificate of type {0} with CA key {1} uses an incompatible signature algorithm {2}
+signCertAlgorithmUnknown=Certificate with CA key {0} is signed with an unknown algorithm {1}
+signCertificateExpired=Expired certificate with CA key {0}
+signCertificateInvalid=Certificate signature does not match on certificate with CA key {0}
+signCertificateNotForName=Certificate with CA key {0} does not apply for name ''{1}''
+signCertificateRevoked=Certificate with CA key {0} was revoked
+signCertificateTooEarly=Certificate with CA key {0} was not valid yet
+signCertificateWithoutPrincipals=Certificate with CA key {0} has no principals; identities from gpg.ssh.allowedSignersFile: {1}
+signDefaultKeyEmpty=git.ssh.defaultKeyCommand {0} returned no key
+signDefaultKeyFailed=git.ssh.defaultKeyCommand {0} failed with exit code {1}\n{2}
+signDefaultKeyInterrupted=git.ssh.defaultKeyCommand {0} was interrupted
+signGarbageAtEnd=SSH signature has extra bytes at the end
+signInvalidAlgorithm=SSH signature has invalid signature algorithm {0}
+signInvalidKeyDSA=SSH signatures with DSA keys or certificates are not supported; use a different signing key.
+signInvalidMagic=SSH signature does not start with "SSHSIG"
+signInvalidNamespace=Namespace of SSH signature should be ''git'' but is ''{0}''
+signInvalidSignature=SSH signature is invalid: {0}
+signInvalidVersion=Cannot verify signature with version {0}
+signKeyExpired=Expired key used for SSH signature
+signKeyRevoked=Key used for the SSH signature was revoked
+signKeyTooEarly=Key used for the SSH signature was not valid yet
+signKrlBlobLeftover=gpg.ssh.revocationFile has invalid blob section {0} with {1} leftover bytes
+signKrlBlobLengthInvalid=gpg.ssh.revocationFile has invalid blob length {1} in section {0}
+signKrlBlobLengthInvalidExpected=gpg.ssh.revocationFile has invalid blob length {1} (expected {2}) in section {0}
+signKrlCaKeyLengthInvalid=gpg.ssh.revocationFile has invalid CA key length {0} in certificates section
+signKrlCertificateLeftover=gpg.ssh.revocationFile has invalid certificates section with {0} leftover bytes
+signKrlCertificateSubsectionLeftover=gpg.ssh.revocationFile has invalid certificates subsection with {0} leftover bytes
+signKrlCertificateSubsectionLength=gpg.ssh.revocationFile has invalid certificates subsection length {0}
+signKrlEmptyRange=gpg.ssh.revocationFile has an empty range of certificate serial numbers
+signKrlInvalidBitSetLength=gpg.ssh.revocationFile has invalid certificate serial number bit set length {0}
+signKrlInvalidKeyIdLength=gpg.ssh.revocationFile has invalid certificate key ID length {0}
+signKrlInvalidMagic=gpg.ssh.revocationFile is not a binary OpenSSH key revocation list
+signKrlInvalidReservedLength=gpg.ssh.revocationFile has an invalid reserved string length {0}
+signKrlInvalidVersion=gpg.ssh.revocationFile: cannot read KRLs with FORMAT_VERSION {0}
+signKrlNoCertificateSubsection=gpg.ssh.revocationFile has certificate section without subsections
+signKrlSerialZero=gpg.ssh.revocationFile: certificate serial number zero cannot be revoked
+signKrlShortRange=gpg.ssh.revocationFile: short certificate serial number range, need at least 8 more bytes, got only {0}
+signKrlUnknownSection=gpg.ssh.revocationFile has an unknown section type {0}
+signKrlUnknownSubsection=gpg.ssh.revocationFile has an unknown certificates subsection type {0}
+signLogFailure=SSH signature verification failed
+signMismatchedSignatureAlgorithm=SSH signature made with an ''{0}'' key has incompatible signature algorithm ''{1}''
+signNoAgent=No connector for ssh-agent found; maybe include org.eclipse.jgit.ssh.apache.agent in the application.
+signNoPrincipalMatched=No principal matched in gpg.ssh.allowedSignersFile
+signNoPublicKey=No public key found with signing key {0}
+signNoSigningKey=Git config user.signingKey or gpg.ssh.defaultKeyCommand must be set for SSH signing.
+signNotUserCertificate=Certificate with CA key {0} used for the SSH signature is not a user certificate.
+signPublicKeyError=Cannot read public key {0}
+signSeeLog=SSH signature verification failed; see the log for details
+signSignatureError=Could not create the signature
+signStderr=Cannot read stderr
+signTooManyPrivateKeys=Private key file {0} must contain exactly one private key
+signTooManyPublicKeys=Public key file {0} must contain exactly one public key
+signUnknownHashAlgorithm=SSH Signature has an unknown hash algorithm {0}
+signUnknownSignatureAlgorithm=SSH Signature has an unknown signature algorithm {0}
+signWrongNamespace=Key may not be used in namespace "{0}".
 unknownProxyProtocol=Ignoring unknown proxy protocol {0}
\ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/AllowedSigners.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/AllowedSigners.java
new file mode 100644
index 0000000..cfbe7a7
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/AllowedSigners.java
@@ -0,0 +1,536 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.signing.ssh.VerificationException;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
+
+/**
+ * Encapsulates the allowed signers handling.
+ */
+final class AllowedSigners extends ModifiableFileWatcher {
+
+	private static final String CERT_AUTHORITY = "cert-authority"; //$NON-NLS-1$
+
+	private static final String NAMESPACES = "namespaces="; //$NON-NLS-1$
+
+	private static final String VALID_AFTER = "valid-after="; //$NON-NLS-1$
+
+	private static final String VALID_BEFORE = "valid-before="; //$NON-NLS-1$
+
+	private static final String SSH_KEY_PREFIX = "ssh-"; //$NON-NLS-1$
+
+	private static final DateTimeFormatter SSH_DATE_FORMAT = new DateTimeFormatterBuilder()
+			.appendValue(ChronoField.YEAR, 4)
+			.appendValue(ChronoField.MONTH_OF_YEAR, 2)
+			.appendValue(ChronoField.DAY_OF_MONTH, 2)
+			.optionalStart()
+			.appendValue(ChronoField.HOUR_OF_DAY, 2)
+			.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+			.optionalStart()
+			.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
+			.toFormatter(Locale.ROOT);
+
+	private static final Predicate<AllowedEntry> CERTIFICATES = AllowedEntry::isCA;
+
+	private static final Predicate<AllowedEntry> PLAIN_KEYS = Predicate
+			.not(CERTIFICATES);
+
+	@SuppressWarnings("ArrayRecordComponent")
+	static record AllowedEntry(String[] identities, boolean isCA,
+			String[] namespaces, Instant validAfter, Instant validBefore,
+			String key) {
+		// Empty
+
+		@Override
+		public final boolean equals(Object any) {
+			if (this == any) {
+				return true;
+			}
+			if (any == null || !(any instanceof AllowedEntry)) {
+				return false;
+			}
+			AllowedEntry other = (AllowedEntry) any;
+			return isCA == other.isCA
+					&& Arrays.equals(identities, other.identities)
+					&& Arrays.equals(namespaces, other.namespaces)
+					&& Objects.equals(validAfter, other.validAfter)
+					&& Objects.equals(validBefore, other.validBefore)
+					&& Objects.equals(key, other.key);
+		}
+
+		@Override
+		public final int hashCode() {
+			int hash = Boolean.hashCode(isCA);
+			hash = hash * 31 + Arrays.hashCode(identities);
+			hash = hash * 31 + Arrays.hashCode(namespaces);
+			return hash * 31 + Objects.hash(validAfter, validBefore, key);
+		}
+	}
+
+	private static record State(Map<String, List<AllowedEntry>> entries) {
+		// Empty
+	}
+
+	private State state;
+
+	public AllowedSigners(Path path) {
+		super(path);
+		state = new State(new HashMap<>());
+	}
+
+	public String isAllowed(PublicKey key, String namespace, String name,
+			Instant time) throws IOException, VerificationException {
+		State currentState = refresh();
+		PublicKey keyToCheck = key;
+		if (key instanceof OpenSshCertificate certificate) {
+			AllowedEntry entry = find(currentState, certificate.getCaPubKey(),
+					namespace, name, time, CERTIFICATES);
+			if (entry != null) {
+				Collection<String> principals = certificate.getPrincipals();
+				if (principals.isEmpty()) {
+					// According to the OpenSSH documentation, a certificate
+					// without principals is valid for anyone.
+					//
+					// See https://man.openbsd.org/ssh-keygen.1#CERTIFICATES .
+					//
+					// However, the same documentation also says that a name
+					// must match both the entry's patterns and be listed in the
+					// certificate's principals.
+					//
+					// See https://man.openbsd.org/ssh-keygen.1#ALLOWED_SIGNERS
+					//
+					// git/OpenSSH considers signatures made by such
+					// certificates untrustworthy.
+					String identities;
+					if (!StringUtils.isEmptyOrNull(name)) {
+						// The name must have matched entry.identities.
+						identities = name;
+					} else {
+						identities = Arrays.stream(entry.identities())
+								.collect(Collectors.joining(",")); //$NON-NLS-1$
+					}
+					throw new VerificationException(false, MessageFormat.format(
+							SshdText.get().signCertificateWithoutPrincipals,
+							KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+							identities));
+				}
+				if (!StringUtils.isEmptyOrNull(name)) {
+					if (!principals.contains(name)) {
+						throw new VerificationException(false,
+								MessageFormat.format(SshdText
+										.get().signCertificateNotForName,
+										KeyUtils.getFingerPrint(
+												certificate.getCaPubKey()),
+										name));
+					}
+					return name;
+				}
+				// Filter the principals listed in the certificate by
+				// the patterns defined in the file.
+				Set<String> filtered = new LinkedHashSet<>();
+				List<String> patterns = Arrays.asList(entry.identities());
+				for (String principal : principals) {
+					if (OpenSshConfigFile.patternMatch(patterns, principal)) {
+						filtered.add(principal);
+					}
+				}
+				return filtered.stream().collect(Collectors.joining(",")); //$NON-NLS-1$
+			}
+			// Certificate not found. git/OpenSSH considers this untrustworthy,
+			// even if the certified key itself might be listed.
+			return null;
+			// Alternative: go check for the certified key itself:
+			// keyToCheck = certificate.getCertPubKey();
+		}
+		AllowedEntry entry = find(currentState, keyToCheck, namespace, name,
+				time, PLAIN_KEYS);
+		if (entry != null) {
+			if (!StringUtils.isEmptyOrNull(name)) {
+				// The name must have matched entry.identities.
+				return name;
+			}
+			// No name given, but we consider the key valid: report the
+			// identities.
+			return Arrays.stream(entry.identities())
+					.collect(Collectors.joining(",")); //$NON-NLS-1$
+		}
+		return null;
+	}
+
+	private AllowedEntry find(State current, PublicKey key,
+			String namespace, String name, Instant time,
+			Predicate<AllowedEntry> filter)
+			throws VerificationException {
+		String k = PublicKeyEntry.toString(key);
+		VerificationException v = null;
+		List<AllowedEntry> candidates = current.entries().get(k);
+		if (candidates == null) {
+			return null;
+		}
+		for (AllowedEntry entry : candidates) {
+			if (!filter.test(entry)) {
+				continue;
+			}
+			if (name != null && !OpenSshConfigFile
+					.patternMatch(Arrays.asList(entry.identities()), name)) {
+				continue;
+			}
+			if (entry.namespaces() != null) {
+				if (!OpenSshConfigFile.patternMatch(
+						Arrays.asList(entry.namespaces()),
+						namespace)) {
+					if (v == null) {
+						v = new VerificationException(false,
+								MessageFormat.format(
+										SshdText.get().signWrongNamespace,
+										KeyUtils.getFingerPrint(key),
+										namespace));
+					}
+					continue;
+				}
+			}
+			if (time != null) {
+				if (entry.validAfter() != null
+						&& time.isBefore(entry.validAfter())) {
+					if (v == null) {
+						v = new VerificationException(true,
+								MessageFormat.format(
+										SshdText.get().signKeyTooEarly,
+										KeyUtils.getFingerPrint(key)));
+					}
+					continue;
+				} else if (entry.validBefore() != null
+						&& time.isAfter(entry.validBefore())) {
+					if (v == null) {
+						v = new VerificationException(true,
+								MessageFormat.format(
+										SshdText.get().signKeyTooEarly,
+										KeyUtils.getFingerPrint(key)));
+					}
+					continue;
+				}
+			}
+			return entry;
+		}
+		if (v != null) {
+			throw v;
+		}
+		return null;
+	}
+
+	private synchronized State refresh() throws IOException {
+		if (checkReloadRequired()) {
+			updateReloadAttributes();
+			try {
+				state = reload(getPath());
+			} catch (NoSuchFileException e) {
+				// File disappeared
+				resetReloadAttributes();
+				state = new State(new HashMap<>());
+			}
+		}
+		return state;
+	}
+
+	private static State reload(Path path) throws IOException {
+		Map<String, List<AllowedEntry>> entries = new HashMap<>();
+		try (BufferedReader r = Files.newBufferedReader(path,
+				StandardCharsets.UTF_8)) {
+			String line;
+			for (int lineNumber = 1;; lineNumber++) {
+				line = r.readLine();
+				if (line == null) {
+					break;
+				}
+				line = line.strip();
+				try {
+					AllowedEntry entry = parseLine(line);
+					if (entry != null) {
+						entries.computeIfAbsent(entry.key(),
+								k -> new ArrayList<>()).add(entry);
+					}
+				} catch (IOException | RuntimeException e) {
+					throw new IOException(MessageFormat.format(
+							SshdText.get().signAllowedSignersFormatError, path,
+							Integer.toString(lineNumber), line), e);
+				}
+			}
+		}
+		return new State(entries);
+	}
+
+	private static boolean matches(String src, String other, int offset) {
+		return src.regionMatches(true, offset, other, 0, other.length());
+	}
+
+	// Things below have package visibility for testing.
+
+	static AllowedEntry parseLine(String line)
+			throws IOException {
+		if (StringUtils.isEmptyOrNull(line) || line.charAt(0) == '#') {
+			return null;
+		}
+		int length = line.length();
+		if ((matches(line, CERT_AUTHORITY, 0)
+				&& CERT_AUTHORITY.length() < length
+				&& Character.isWhitespace(line.charAt(CERT_AUTHORITY.length())))
+				|| matches(line, NAMESPACES, 0)
+				|| matches(line, VALID_AFTER, 0)
+				|| matches(line, VALID_BEFORE, 0)
+				|| matches(line, SSH_KEY_PREFIX, 0)) {
+			throw new StreamCorruptedException(
+					SshdText.get().signAllowedSignersNoIdentities);
+		}
+		int i = 0;
+		while (i < length && !Character.isWhitespace(line.charAt(i))) {
+			i++;
+		}
+		if (i >= length) {
+			throw new StreamCorruptedException(SshdText.get().signAllowedSignersLineFormat);
+		}
+		String[] identities = line.substring(0, i).split(","); //$NON-NLS-1$
+		if (Arrays.stream(identities).anyMatch(String::isEmpty)) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersEmptyIdentity,
+					line.substring(0, i)));
+		}
+		// Parse the options
+		i++;
+		boolean isCA = false;
+		List<String> namespaces = null;
+		Instant validAfter = null;
+		Instant validBefore = null;
+		while (i < length) {
+			// Skip whitespace
+			if (Character.isSpaceChar(line.charAt(i))) {
+				i++;
+				continue;
+			}
+			if (matches(line, CERT_AUTHORITY, i)) {
+				i += CERT_AUTHORITY.length();
+				isCA = true;
+				if (!Character.isWhitespace(line.charAt(i))) {
+					throw new StreamCorruptedException(SshdText.get().signAllowedSignersCertAuthorityError);
+				}
+				i++;
+			} else if (matches(line, NAMESPACES, i)) {
+				if (namespaces != null) {
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signAllowedSignersMultiple,
+							NAMESPACES));
+				}
+				i += NAMESPACES.length();
+				Dequoted parsed = dequote(line, i);
+				i = parsed.after();
+				String ns = parsed.value();
+				String[] items = ns.split(","); //$NON-NLS-1$
+				namespaces = new ArrayList<>(items.length);
+				for (int j = 0; j < items.length; j++) {
+					String n = items[j].strip();
+					if (!n.isEmpty()) {
+						namespaces.add(n);
+					}
+				}
+				if (namespaces.isEmpty()) {
+					throw new StreamCorruptedException(
+							SshdText.get().signAllowedSignersEmptyNamespaces);
+				}
+			} else if (matches(line, VALID_AFTER, i)) {
+				if (validAfter != null) {
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signAllowedSignersMultiple,
+							VALID_AFTER));
+				}
+				i += VALID_AFTER.length();
+				Dequoted parsed = dequote(line, i);
+				i = parsed.after();
+				validAfter = parseDate(parsed.value());
+			} else if (matches(line, VALID_BEFORE, i)) {
+				if (validBefore != null) {
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signAllowedSignersMultiple,
+							VALID_BEFORE));
+				}
+				i += VALID_BEFORE.length();
+				Dequoted parsed = dequote(line, i);
+				i = parsed.after();
+				validBefore = parseDate(parsed.value());
+			} else {
+				break;
+			}
+		}
+		// Now we should be at the key
+		String key = parsePublicKey(line, i);
+		return new AllowedEntry(identities, isCA,
+				namespaces == null ? null : namespaces.toArray(new String[0]),
+				validAfter, validBefore, key);
+	}
+
+	static String parsePublicKey(String s, int from)
+			throws StreamCorruptedException {
+		int i = from;
+		int length = s.length();
+		while (i < length && Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		if (i >= length) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(from)));
+		}
+		int start = i;
+		while (i < length && !Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		if (i >= length) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(start)));
+		}
+		int endOfKeyType = i;
+		i = endOfKeyType + 1;
+		while (i < length && Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		int startOfKey = i;
+		while (i < length && !Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		if (i == startOfKey) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(start)));
+		}
+		String keyType = s.substring(start, endOfKeyType);
+		if (!keyType.startsWith(SSH_KEY_PREFIX)) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(start)));
+		}
+		return keyType + ' ' + s.substring(startOfKey, i);
+	}
+
+	static Instant parseDate(String input) {
+		// Allowed formats are YYYYMMDD[Z] or YYYYMMDDHHMM[SS][Z]. If 'Z', it's
+		// UTC, otherwise local time.
+		String timeSpec = input;
+		int length = input.length();
+		if (length < 8) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					SshdText.get().signAllowedSignersInvalidDate, input));
+		}
+		boolean isUTC = false;
+		if (timeSpec.charAt(length - 1) == 'Z') {
+			isUTC = true;
+			timeSpec = timeSpec.substring(0, length - 1);
+		}
+		LocalDateTime time;
+		TemporalAccessor temporalAccessor = SSH_DATE_FORMAT.parseBest(timeSpec,
+				LocalDateTime::from, LocalDate::from);
+		if (temporalAccessor instanceof LocalDateTime) {
+			time = (LocalDateTime) temporalAccessor;
+		} else {
+			time = ((LocalDate) temporalAccessor).atStartOfDay();
+		}
+		if (isUTC) {
+			return time.atOffset(ZoneOffset.UTC).toInstant();
+		}
+		TimeZone tz = SystemReader.getInstance().getTimeZone();
+		// Since there are a few TimeZone IDs that are not recognized by ZoneId,
+		// use offsets.
+		return time.atOffset(ZoneOffset.ofTotalSeconds(
+				(int) TimeUnit.MILLISECONDS.toSeconds(tz.getRawOffset())))
+				.toInstant();
+	}
+
+	// OpenSSH uses the backslash *only* to quote the double-quote.
+	static Dequoted dequote(String line, int from) {
+		int length = line.length();
+		int i = from;
+		if (line.charAt(i) == '"') {
+			boolean quoted = false;
+			i++;
+			StringBuilder b = new StringBuilder();
+			while (i < length) {
+				char ch = line.charAt(i);
+				if (ch == '"') {
+					if (quoted) {
+						b.append(ch);
+						quoted = false;
+					} else {
+						break;
+					}
+				} else if (ch == '\\') {
+					quoted = true;
+				} else {
+					if (quoted) {
+						b.append('\\');
+					}
+					b.append(ch);
+					quoted = false;
+				}
+				i++;
+			}
+			if (i >= length) {
+				throw new IllegalArgumentException(
+						SshdText.get().signAllowedSignersUnterminatedQuote);
+			}
+			return new Dequoted(b.toString(), i + 1);
+		}
+		while (i < length && !Character.isWhitespace(line.charAt(i))) {
+			i++;
+		}
+		return new Dequoted(line.substring(from, i), i);
+	}
+
+	static record Dequoted(String value, int after) {
+		// Empty
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrl.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrl.java
new file mode 100644
index 0000000..6b19eb32
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrl.java
@@ -0,0 +1,491 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * An implementation of OpenSSH binary format key revocation lists (KRLs).
+ *
+ * @see <a href=
+ *      "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.krl">PROTOCOL.krl</a>
+ */
+class OpenSshBinaryKrl {
+
+	/**
+	 * The "magic" bytes at the start of an OpenSSH binary KRL.
+	 */
+	static final byte[] MAGIC = { 'S', 'S', 'H', 'K', 'R', 'L', '\n', 0 };
+
+	private static final int FORMAT_VERSION = 1;
+
+	private static final int SECTION_CERTIFICATES = 1;
+
+	private static final int SECTION_KEY = 2;
+
+	private static final int SECTION_SHA1 = 3;
+
+	private static final int SECTION_SIGNATURE = 4; // Skipped
+
+	private static final int SECTION_SHA256 = 5;
+
+	private static final int SECTION_EXTENSION = 255; // Skipped
+
+	// Certificates
+
+	private static final int CERT_SERIAL_LIST = 0x20;
+
+	private static final int CERT_SERIAL_RANGES = 0x21;
+
+	private static final int CERT_SERIAL_BITS = 0x22;
+
+	private static final int CERT_KEY_IDS = 0x23;
+
+	private static final int CERT_EXTENSIONS = 0x39; // Skipped
+
+	private final Map<Blob, CertificateRevocation> certificates = new HashMap<>();
+
+	private static class CertificateRevocation {
+
+		final SerialRangeSet ranges = new SerialRangeSet();
+
+		final Set<String> keyIds = new HashSet<>();
+	}
+
+	// Plain keys
+
+	/**
+	 * A byte array that can be used as a key in a {@link Map} or {@link Set}.
+	 * {@link #equals(Object)} and {@link #hashCode()} are based on the content.
+	 *
+	 * @param blob
+	 *            the array to wrap
+	 */
+	@SuppressWarnings("ArrayRecordComponent")
+	private static record Blob(byte[] blob) {
+
+		@Override
+		public final boolean equals(Object any) {
+			if (this == any) {
+				return true;
+			}
+			if (any == null || !(any instanceof Blob)) {
+				return false;
+			}
+			Blob other = (Blob) any;
+			return Arrays.equals(blob, other.blob);
+		}
+
+		@Override
+		public final int hashCode() {
+			return Arrays.hashCode(blob);
+		}
+	}
+
+	private final Set<Blob> blobs = new HashSet<>();
+
+	private final Set<Blob> sha1 = new HashSet<>();
+
+	private final Set<Blob> sha256 = new HashSet<>();
+
+	private OpenSshBinaryKrl() {
+		// No public instantiation, use load(InputStream, boolean) instead.
+	}
+
+	/**
+	 * Tells whether the given key has been revoked.
+	 *
+	 * @param key
+	 *            {@link PublicKey} to check
+	 * @return {@code true} if the key was revoked, {@code false} otherwise
+	 */
+	boolean isRevoked(PublicKey key) {
+		if (key instanceof OpenSshCertificate certificate) {
+			if (certificates.isEmpty()) {
+				return false;
+			}
+			// These apply to all certificates
+			if (isRevoked(certificate, certificates.get(null))) {
+				return true;
+			}
+			if (isRevoked(certificate,
+					certificates.get(blob(certificate.getCaPubKey())))) {
+				return true;
+			}
+			// Keys themselves are checked in OpenSshKrl.
+			return false;
+		}
+		if (!blobs.isEmpty() && blobs.contains(blob(key))) {
+			return true;
+		}
+		if (!sha256.isEmpty() && sha256.contains(hash("SHA256", key))) { //$NON-NLS-1$
+			return true;
+		}
+		if (!sha1.isEmpty() && sha1.contains(hash("SHA1", key))) { //$NON-NLS-1$
+			return true;
+		}
+		return false;
+	}
+
+	private boolean isRevoked(OpenSshCertificate certificate,
+			CertificateRevocation revocations) {
+		if (revocations == null) {
+			return false;
+		}
+		String id = certificate.getId();
+		if (!StringUtils.isEmptyOrNull(id) && revocations.keyIds.contains(id)) {
+			return true;
+		}
+		long serial = certificate.getSerial();
+		if (serial != 0 && revocations.ranges.contains(serial)) {
+			return true;
+		}
+		return false;
+	}
+
+	private Blob blob(PublicKey key) {
+		ByteArrayBuffer buf = new ByteArrayBuffer();
+		buf.putRawPublicKey(key);
+		return new Blob(buf.getCompactData());
+	}
+
+	private Blob hash(String algorithm, PublicKey key) {
+		ByteArrayBuffer buf = new ByteArrayBuffer();
+		buf.putRawPublicKey(key);
+		try {
+			return new Blob(MessageDigest.getInstance(algorithm)
+					.digest(buf.getCompactData()));
+		} catch (NoSuchAlgorithmException e) {
+			throw new JGitInternalException(e.getMessage(), e);
+		}
+	}
+
+	/**
+	 * Loads a binary KRL from the given stream.
+	 *
+	 * @param in
+	 *            {@link InputStream} to read from
+	 * @param magicSkipped
+	 *            whether the {@link #MAGIC} bytes at the beginning have already
+	 *            been skipped
+	 * @return a new {@link OpenSshBinaryKrl}.
+	 * @throws IOException
+	 *             if the stream cannot be read as an OpenSSH binary KRL
+	 */
+	@NonNull
+	static OpenSshBinaryKrl load(InputStream in, boolean magicSkipped)
+			throws IOException {
+		if (!magicSkipped) {
+			byte[] magic = new byte[MAGIC.length];
+			IO.readFully(in, magic);
+			if (!Arrays.equals(magic, MAGIC)) {
+				throw new StreamCorruptedException(
+						SshdText.get().signKrlInvalidMagic);
+			}
+		}
+		skipHeader(in);
+		return load(in);
+	}
+
+	private static long getUInt(InputStream in) throws IOException {
+		byte[] buf = new byte[Integer.BYTES];
+		IO.readFully(in, buf);
+		return BufferUtils.getUInt(buf);
+	}
+
+	private static long getLong(InputStream in) throws IOException {
+		byte[] buf = new byte[Long.BYTES];
+		IO.readFully(in, buf);
+		return BufferUtils.getLong(buf, 0, Long.BYTES);
+	}
+
+	private static void skipHeader(InputStream in) throws IOException {
+		long version = getUInt(in);
+		if (version != FORMAT_VERSION) {
+			throw new StreamCorruptedException(
+					MessageFormat.format(SshdText.get().signKrlInvalidVersion,
+							Long.valueOf(version)));
+		}
+		// krl_version, generated_date, flags (none defined in version 1)
+		in.skip(24);
+		in.skip(getUInt(in)); // reserved
+		in.skip(getUInt(in)); // comment
+	}
+
+	private static OpenSshBinaryKrl load(InputStream in) throws IOException {
+		OpenSshBinaryKrl krl = new OpenSshBinaryKrl();
+		for (;;) {
+			int sectionType = in.read();
+			if (sectionType < 0) {
+				break; // EOF
+			}
+			switch (sectionType) {
+			case SECTION_CERTIFICATES:
+				readCertificates(krl.certificates, in, getUInt(in));
+				break;
+			case SECTION_KEY:
+				readBlobs("explicit_keys", krl.blobs, in, getUInt(in), 0); //$NON-NLS-1$
+				break;
+			case SECTION_SHA1:
+				readBlobs("fingerprint_sha1", krl.sha1, in, getUInt(in), 20); //$NON-NLS-1$
+				break;
+			case SECTION_SIGNATURE:
+				// Unsupported as of OpenSSH 9.4. It even refuses to load such
+				// KRLs. Just skip it.
+				in.skip(getUInt(in));
+				break;
+			case SECTION_SHA256:
+				readBlobs("fingerprint_sha256", krl.sha256, in, getUInt(in), //$NON-NLS-1$
+						32);
+				break;
+			case SECTION_EXTENSION:
+				// No extensions are defined for version 1 KRLs.
+				in.skip(getUInt(in));
+				break;
+			default:
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlUnknownSection,
+						Integer.valueOf(sectionType)));
+			}
+		}
+		return krl;
+	}
+
+	private static void readBlobs(String sectionName, Set<Blob> blobs,
+			InputStream in, long sectionLength, long expectedBlobLength)
+			throws IOException {
+		while (sectionLength >= Integer.BYTES) {
+			// Read blobs.
+			long blobLength = getUInt(in);
+			sectionLength -= Integer.BYTES;
+			if (blobLength > sectionLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlBlobLengthInvalid, sectionName,
+						Long.valueOf(blobLength)));
+			}
+			if (expectedBlobLength != 0 && blobLength != expectedBlobLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlBlobLengthInvalidExpected,
+						sectionName, Long.valueOf(blobLength),
+						Long.valueOf(expectedBlobLength)));
+			}
+			byte[] blob = new byte[(int) blobLength];
+			IO.readFully(in, blob);
+			sectionLength -= blobLength;
+			blobs.add(new Blob(blob));
+		}
+		if (sectionLength != 0) {
+			throw new StreamCorruptedException(
+					MessageFormat.format(SshdText.get().signKrlBlobLeftover,
+							sectionName, Long.valueOf(sectionLength)));
+		}
+	}
+
+	private static void readCertificates(Map<Blob, CertificateRevocation> certs,
+			InputStream in, long sectionLength) throws IOException {
+		long keyLength = getUInt(in);
+		sectionLength -= Integer.BYTES;
+		if (keyLength > sectionLength) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCaKeyLengthInvalid,
+					Long.valueOf(keyLength)));
+		}
+		Blob key = null;
+		if (keyLength > 0) {
+			byte[] blob = new byte[(int) keyLength];
+			IO.readFully(in, blob);
+			key = new Blob(blob);
+			sectionLength -= keyLength;
+		}
+		CertificateRevocation rev = certs.computeIfAbsent(key,
+				k -> new CertificateRevocation());
+		long reservedLength = getUInt(in);
+		sectionLength -= Integer.BYTES;
+		if (reservedLength > sectionLength) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCaKeyLengthInvalid,
+					Long.valueOf(reservedLength)));
+		}
+		in.skip(reservedLength);
+		sectionLength -= reservedLength;
+		if (sectionLength == 0) {
+			throw new StreamCorruptedException(
+					SshdText.get().signKrlNoCertificateSubsection);
+		}
+		while (sectionLength > 0) {
+			int subSection = in.read();
+			if (subSection < 0) {
+				throw new EOFException();
+			}
+			sectionLength--;
+			if (sectionLength < Integer.BYTES) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateLeftover,
+						Long.valueOf(sectionLength)));
+			}
+			long subLength = getUInt(in);
+			sectionLength -= Integer.BYTES;
+			if (subLength > sectionLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateSubsectionLength,
+						Long.valueOf(subLength)));
+			}
+			if (subLength > 0) {
+				switch (subSection) {
+				case CERT_SERIAL_LIST:
+					readSerials(rev.ranges, in, subLength, false);
+					break;
+				case CERT_SERIAL_RANGES:
+					readSerials(rev.ranges, in, subLength, true);
+					break;
+				case CERT_SERIAL_BITS:
+					readSerialBitSet(rev.ranges, in, subLength);
+					break;
+				case CERT_KEY_IDS:
+					readIds(rev.keyIds, in, subLength);
+					break;
+				case CERT_EXTENSIONS:
+					in.skip(subLength);
+					break;
+				default:
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signKrlUnknownSubsection,
+							Long.valueOf(subSection)));
+				}
+			}
+			sectionLength -= subLength;
+		}
+	}
+
+	private static void readSerials(SerialRangeSet set, InputStream in,
+			long length, boolean ranges) throws IOException {
+		while (length >= Long.BYTES) {
+			long a = getLong(in);
+			length -= Long.BYTES;
+			if (a == 0) {
+				throw new StreamCorruptedException(
+						SshdText.get().signKrlSerialZero);
+			}
+			if (!ranges) {
+				set.add(a);
+				continue;
+			}
+			if (length < Long.BYTES) {
+				throw new StreamCorruptedException(
+						MessageFormat.format(SshdText.get().signKrlShortRange,
+								Long.valueOf(length)));
+			}
+			long b = getLong(in);
+			length -= Long.BYTES;
+			if (Long.compareUnsigned(a, b) > 0) {
+				throw new StreamCorruptedException(
+						SshdText.get().signKrlEmptyRange);
+			}
+			set.add(a, b);
+		}
+		if (length != 0) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCertificateSubsectionLeftover,
+					Long.valueOf(length)));
+		}
+	}
+
+	private static void readSerialBitSet(SerialRangeSet set, InputStream in,
+			long subLength) throws IOException {
+		while (subLength > 0) {
+			if (subLength < Long.BYTES) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateSubsectionLeftover,
+						Long.valueOf(subLength)));
+			}
+			long base = getLong(in);
+			subLength -= Long.BYTES;
+			if (subLength < Integer.BYTES) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateSubsectionLeftover,
+						Long.valueOf(subLength)));
+			}
+			long setLength = getUInt(in);
+			subLength -= Integer.BYTES;
+			if (setLength == 0 || setLength > subLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlInvalidBitSetLength,
+						Long.valueOf(setLength)));
+			}
+			// Now process the bits. Note that the mpint is stored MSB first.
+			//
+			// We set individual serial numbers (one for each set bit) and let
+			// the SerialRangeSet take care of coalescing for successive runs
+			// of set bits.
+			int n = (int) setLength;
+			for (int i = n - 1; i >= 0; i--) {
+				int b = in.read();
+				if (b < 0) {
+					throw new EOFException();
+				} else if (b == 0) {
+					// Stored as an mpint: may have leading zero bytes (actually
+					// at most one; if the high bit of the first byte is set).
+					continue;
+				}
+				for (int bit = 0,
+						mask = 1; bit < Byte.SIZE; bit++, mask <<= 1) {
+					if ((b & mask) != 0) {
+						set.add(base + (i * Byte.SIZE) + bit);
+					}
+				}
+			}
+			subLength -= setLength;
+		}
+	}
+
+	private static void readIds(Set<String> ids, InputStream in, long subLength)
+			throws IOException {
+		while (subLength >= Integer.BYTES) {
+			long length = getUInt(in);
+			subLength -= Integer.BYTES;
+			if (length > subLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlInvalidKeyIdLength,
+						Long.valueOf(length)));
+			}
+			byte[] bytes = new byte[(int) length];
+			IO.readFully(in, bytes);
+			ids.add(new String(bytes, StandardCharsets.UTF_8));
+			subLength -= length;
+		}
+		if (subLength != 0) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCertificateSubsectionLeftover,
+					Long.valueOf(subLength)));
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshKrl.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshKrl.java
new file mode 100644
index 0000000..7993def
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshKrl.java
@@ -0,0 +1,120 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+import org.eclipse.jgit.util.IO;
+
+/**
+ * An implementation of an OpenSSH key revocation list (KRL), either a binary
+ * KRL or a simple list of public keys.
+ */
+class OpenSshKrl extends ModifiableFileWatcher {
+
+	private static record State(Set<String> keys, OpenSshBinaryKrl krl) {
+		// Empty
+	}
+
+	private State state;
+
+	public OpenSshKrl(Path path) {
+		super(path);
+		state = new State(Set.of(), null);
+	}
+
+	public boolean isRevoked(PublicKey key) throws IOException {
+		State current = refresh();
+		return isRevoked(current, key);
+	}
+
+	private boolean isRevoked(State current, PublicKey key) {
+		if (key instanceof OpenSshCertificate cert) {
+			OpenSshBinaryKrl krl = current.krl();
+			if (krl != null && krl.isRevoked(cert)) {
+				return true;
+			}
+			if (isRevoked(current, cert.getCaPubKey())
+					|| isRevoked(current, cert.getCertPubKey())) {
+				return true;
+			}
+			return false;
+		}
+		OpenSshBinaryKrl krl = current.krl();
+		if (krl != null) {
+			return krl.isRevoked(key);
+		}
+		return current.keys().contains(PublicKeyEntry.toString(key));
+	}
+
+	private synchronized State refresh() throws IOException {
+		if (checkReloadRequired()) {
+			updateReloadAttributes();
+			try {
+				state = reload(getPath());
+			} catch (NoSuchFileException e) {
+				// File disappeared
+				resetReloadAttributes();
+				state = new State(Set.of(), null);
+			}
+		}
+		return state;
+	}
+
+	private static State reload(Path path) throws IOException {
+		try (BufferedInputStream in = new BufferedInputStream(
+				Files.newInputStream(path))) {
+			byte[] magic = new byte[OpenSshBinaryKrl.MAGIC.length];
+			in.mark(magic.length);
+			IO.readFully(in, magic);
+			if (Arrays.equals(magic, OpenSshBinaryKrl.MAGIC)) {
+				return new State(null, OpenSshBinaryKrl.load(in, true));
+			}
+			// Otherwise try reading it textually
+			in.reset();
+			return loadTextKrl(in);
+		}
+	}
+
+	private static State loadTextKrl(InputStream in) throws IOException {
+		Set<String> keys = new HashSet<>();
+		try (BufferedReader r = new BufferedReader(
+				new InputStreamReader(in, StandardCharsets.UTF_8))) {
+			String line;
+			for (;;) {
+				line = r.readLine();
+				if (line == null) {
+					break;
+				}
+				line = line.strip();
+				if (line.isEmpty() || line.charAt(0) == '#') {
+					continue;
+				}
+				keys.add(AllowedSigners.parsePublicKey(line, 0));
+			}
+		}
+		return new State(keys, null);
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshSigningKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshSigningKeyDatabase.java
new file mode 100644
index 0000000..aa26886
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshSigningKeyDatabase.java
@@ -0,0 +1,161 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.signing.ssh.CachingSigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.VerificationException;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * A {@link CachingSigningKeyDatabase} using the OpenSSH allowed signers file
+ * and the OpenSSH key revocation list.
+ */
+public class OpenSshSigningKeyDatabase implements CachingSigningKeyDatabase {
+
+	// Keep caches of allowed signers and KRLs. Cache by canonical path.
+
+	private static final int DEFAULT_CACHE_SIZE = 5;
+
+	private AtomicInteger cacheSize = new AtomicInteger(DEFAULT_CACHE_SIZE);
+
+	private class LRU<K, V> extends LinkedHashMap<K, V> {
+
+		private static final long serialVersionUID = 1L;
+
+		LRU() {
+			super(DEFAULT_CACHE_SIZE, 0.75f, true);
+		}
+
+		@Override
+		protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
+			return size() > cacheSize.get();
+		}
+	}
+
+	private final HashMap<Path, AllowedSigners> allowedSigners = new LRU<>();
+
+	private final HashMap<Path, OpenSshKrl> revocations = new LRU<>();
+
+	@Override
+	public boolean isRevoked(Repository repository, GpgConfig config,
+			PublicKey key) throws IOException {
+		String fileName = config.getSshRevocationFile();
+		if (StringUtils.isEmptyOrNull(fileName)) {
+			return false;
+		}
+		File file = getFile(repository, fileName);
+		OpenSshKrl revocationList;
+		synchronized (revocations) {
+			revocationList = revocations.computeIfAbsent(file.toPath(),
+					OpenSshKrl::new);
+		}
+		return revocationList.isRevoked(key);
+	}
+
+	@Override
+	public String isAllowed(Repository repository, GpgConfig config,
+			PublicKey key, String namespace, PersonIdent ident)
+			throws IOException, VerificationException {
+		String fileName = config.getSshAllowedSignersFile();
+		if (StringUtils.isEmptyOrNull(fileName)) {
+			// No file configured. Git would error out.
+			return null;
+		}
+		File file = getFile(repository, fileName);
+		AllowedSigners allowed;
+		synchronized (allowedSigners) {
+			allowed = allowedSigners.computeIfAbsent(file.toPath(),
+					AllowedSigners::new);
+		}
+		Instant gitTime = null;
+		if (ident != null) {
+			gitTime = ident.getWhenAsInstant();
+		}
+		return allowed.isAllowed(key, namespace, null, gitTime);
+	}
+
+	private File getFile(@NonNull Repository repository, String fileName)
+			throws IOException {
+		File file;
+		if (fileName.startsWith("~/") //$NON-NLS-1$
+				|| fileName.startsWith('~' + File.separator)) {
+			file = FS.DETECTED.resolve(FS.DETECTED.userHome(),
+					fileName.substring(2));
+		} else {
+			file = new File(fileName);
+			if (!file.isAbsolute()) {
+				file = new File(repository.getWorkTree(), fileName);
+			}
+		}
+		return file.getCanonicalFile();
+	}
+
+	@Override
+	public int getCacheSize() {
+		return cacheSize.get();
+	}
+
+	@Override
+	public void setCacheSize(int size) {
+		if (size > 0) {
+			cacheSize.set(size);
+			pruneCache(size);
+		}
+	}
+
+	private void pruneCache(int size) {
+		prune(allowedSigners, size);
+		prune(revocations, size);
+	}
+
+	private void prune(HashMap<?, ?> map, int size) {
+		synchronized (map) {
+			if (map.size() <= size) {
+				return;
+			}
+			Iterator<?> iter = map.entrySet().iterator();
+			int i = 0;
+			while (iter.hasNext() && i < size) {
+				iter.next();
+				i++;
+			}
+			while (iter.hasNext()) {
+				iter.next();
+				iter.remove();
+			}
+		}
+	}
+
+	@Override
+	public void clearCache() {
+		synchronized (allowedSigners) {
+			allowedSigners.clear();
+		}
+		synchronized (revocations) {
+			revocations.clear();
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SerialRangeSet.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SerialRangeSet.java
new file mode 100644
index 0000000..f4eb884
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SerialRangeSet.java
@@ -0,0 +1,131 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.util.TreeMap;
+
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+
+/**
+ * Encapsulates the storage for revoked certificate serial numbers.
+ */
+class SerialRangeSet {
+
+	/**
+	 * A range of certificate serial numbers [from..to], i.e., with both range
+	 * limits included.
+	 */
+	private interface SerialRange {
+
+		long from();
+
+		long to();
+	}
+
+	private static record Singleton(long from) implements SerialRange {
+
+		@Override
+		public long to() {
+			return from;
+		}
+	}
+
+	private static record Range(long from, long to) implements SerialRange {
+
+		public Range(long from, long to) {
+			if (Long.compareUnsigned(from, to) > 0) {
+				throw new IllegalArgumentException(
+						SshdText.get().signKrlEmptyRange);
+			}
+			this.from = from;
+			this.to = to;
+		}
+	}
+
+	// We use the same data structure as OpenSSH; basically a TreeSet of mutable
+	// SerialRanges. To get "mutability", the set is implemented as a TreeMap
+	// with the same elements as keys and values.
+	//
+	// get(x) will return null if none of the serial numbers in the range x is
+	// in the set, and some range (partially) overlapping with x otherwise.
+	//
+	// containsKey(x) will return true if there is any (partially) overlapping
+	// range in the TreeMap.
+	private final TreeMap<SerialRange, SerialRange> ranges = new TreeMap<>(
+			SerialRangeSet::compare);
+
+	private static int compare(SerialRange a, SerialRange b) {
+		// Return == if they overlap
+		if (Long.compareUnsigned(a.to(), b.from()) >= 0
+				&& Long.compareUnsigned(a.from(), b.to()) <= 0) {
+			return 0;
+		}
+		return Long.compareUnsigned(a.from(), b.from());
+	}
+
+	void add(long serial) {
+		add(ranges, new Singleton(serial));
+	}
+
+	void add(long from, long to) {
+		add(ranges, new Range(from, to));
+	}
+
+	boolean contains(long serial) {
+		return ranges.containsKey(new Singleton(serial));
+	}
+
+	int size() {
+		return ranges.size();
+	}
+
+	boolean isEmpty() {
+		return ranges.isEmpty();
+	}
+
+	private static void add(TreeMap<SerialRange, SerialRange> ranges,
+			SerialRange newRange) {
+		for (;;) {
+			SerialRange existing = ranges.get(newRange);
+			if (existing == null) {
+				break;
+			}
+			if (Long.compareUnsigned(existing.from(), newRange.from()) <= 0
+					&& Long.compareUnsigned(existing.to(),
+							newRange.to()) >= 0) {
+				// newRange completely contained in existing
+				return;
+			}
+			ranges.remove(existing);
+			long newFrom = newRange.from();
+			if (Long.compareUnsigned(existing.from(), newFrom) < 0) {
+				newFrom = existing.from();
+			}
+			long newTo = newRange.to();
+			if (Long.compareUnsigned(existing.to(), newTo) > 0) {
+				newTo = existing.to();
+			}
+			newRange = new Range(newFrom, newTo);
+		}
+		// No overlapping range exists: check for coalescing with the
+		// previous/next range
+		SerialRange prev = ranges.floorKey(newRange);
+		if (prev != null && newRange.from() - prev.to() == 1) {
+			ranges.remove(prev);
+			newRange = new Range(prev.from(), newRange.to());
+		}
+		SerialRange next = ranges.ceilingKey(newRange);
+		if (next != null && next.from() - newRange.to() == 1) {
+			ranges.remove(next);
+			newRange = new Range(newRange.from(), next.to());
+		}
+		ranges.put(newRange, newRange);
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SigningDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SigningDatabase.java
new file mode 100644
index 0000000..e2e1a36
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SigningDatabase.java
@@ -0,0 +1,59 @@
+/*
+ * 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.internal.signing.ssh;
+
+import org.eclipse.jgit.signing.ssh.CachingSigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.SigningKeyDatabase;
+
+/**
+ * A global {@link SigningKeyDatabase} instance.
+ */
+public final class SigningDatabase {
+
+	private static SigningKeyDatabase INSTANCE = new OpenSshSigningKeyDatabase();
+
+	private SigningDatabase() {
+		// No instantiation
+	}
+
+	/**
+	 * Obtains the current instance.
+	 *
+	 * @return the global {@link SigningKeyDatabase}
+	 */
+	public static synchronized SigningKeyDatabase getInstance() {
+		return INSTANCE;
+	}
+
+	/**
+	 * Sets the global {@link SigningKeyDatabase}.
+	 *
+	 * @param database
+	 *            to set; if {@code null} a default database using the OpenSSH
+	 *            allowed signers file and the OpenSSH revocation list mechanism
+	 *            is used.
+	 * @return the previously set {@link SigningKeyDatabase}
+	 */
+	public static synchronized SigningKeyDatabase setInstance(
+			SigningKeyDatabase database) {
+		SigningKeyDatabase previous = INSTANCE;
+		if (database != INSTANCE) {
+			if (INSTANCE instanceof CachingSigningKeyDatabase caching) {
+				caching.clearCache();
+			}
+			if (database == null) {
+				INSTANCE = new OpenSshSigningKeyDatabase();
+			} else {
+				INSTANCE = database;
+			}
+		}
+		return previous;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java
new file mode 100644
index 0000000..040c6d4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java
@@ -0,0 +1,175 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for working with OpenSSH certificates.
+ */
+final class SshCertificateUtils {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(SshCertificateUtils.class);
+
+	/**
+	 * Verifies a certificate: checks that it is a user certificate and has a
+	 * valid signature, and if a time is given, that the certificate is valid at
+	 * that time.
+	 *
+	 * @param certificate
+	 *            {@link OpenSshCertificate} to verify
+	 * @param signatureTime
+	 *            {@link Instant} to check whether the certificate is valid at
+	 *            that time; maybe {@code null}, in which case the valid-time
+	 *            check is skipped.
+	 * @return {@code null} if the certificate is valid; otherwise a descriptive
+	 *         message
+	 */
+	static String verify(OpenSshCertificate certificate,
+			Instant signatureTime) {
+		if (!OpenSshCertificate.Type.USER.equals(certificate.getType())) {
+			return MessageFormat.format(SshdText.get().signNotUserCertificate,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+		}
+		String message = verifySignature(certificate);
+		if (message == null && signatureTime != null) {
+			message = checkExpiration(certificate, signatureTime);
+		}
+		return message;
+	}
+
+	/**
+	 * Verifies the signature on a certificate.
+	 *
+	 * @param certificate
+	 *            {@link OpenSshCertificate} to verify
+	 * @return {@code null} if the signature is valid; otherwise a descriptive
+	 *         message
+	 */
+	static String verifySignature(OpenSshCertificate certificate) {
+		// Verify the signature on the certificate.
+		//
+		// Note that OpenSSH certificates do not support chaining.
+		//
+		// ssh-keygen refuses to create a certificate for a certificate, so the
+		// certified key cannot be another OpenSshCertificate. Additionally,
+		// when creating a certificate ssh-keygen loads the CA private key to
+		// make the signature and reconstructs the public key that it stores in
+		// the certificate from that, so the CA public key also cannot be an
+		// OpenSshCertificate.
+		PublicKey caKey = certificate.getCaPubKey();
+		PublicKey certifiedKey = certificate.getCertPubKey();
+		if (caKey == null
+				|| caKey instanceof OpenSshCertificate
+				|| certifiedKey == null
+				|| certifiedKey instanceof OpenSshCertificate) {
+			return SshdText.get().signCertificateInvalid;
+		}
+		// Verify that key type and algorithm match
+		String keyType = KeyUtils.getKeyType(caKey);
+		String certAlgorithm = certificate.getSignatureAlgorithm();
+		if (!KeyUtils.getCanonicalKeyType(keyType)
+				.equals(KeyUtils.getCanonicalKeyType(certAlgorithm))) {
+			return MessageFormat.format(
+					SshdText.get().signCertAlgorithmMismatch, keyType,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+					certAlgorithm);
+		}
+		BuiltinSignatures factory = BuiltinSignatures
+				.fromFactoryName(certAlgorithm);
+		if (factory == null || !factory.isSupported()) {
+			return MessageFormat.format(SshdText.get().signCertAlgorithmUnknown,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+					certAlgorithm);
+		}
+		Signature signer = factory.create();
+		try {
+			signer.initVerifier(null, caKey);
+			signer.update(null, getBlob(certificate));
+			if (signer.verify(null, certificate.getRawSignature())) {
+				return null;
+			}
+		} catch (Exception e) {
+			LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+			return SshdText.get().signSeeLog;
+		}
+		return MessageFormat.format(SshdText.get().signCertificateInvalid,
+				KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+	}
+
+	private static byte[] getBlob(OpenSshCertificate certificate) {
+		// Theoretically, this should be just certificate.getMessage(). But
+		// Apache MINA sshd has a bug and may return additional bytes if the
+		// certificate is not the first thing in the buffer it was read from.
+		// As a work-around, re-create the signed blob from scratch.
+		//
+		// This may be replaced by return certificate.getMessage() once the
+		// upstream bug is fixed.
+		//
+		// See https://github.com/apache/mina-sshd/issues/618
+		Buffer tmp = new ByteArrayBuffer();
+		tmp.putString(certificate.getKeyType());
+		tmp.putBytes(certificate.getNonce());
+		tmp.putRawPublicKeyBytes(certificate.getCertPubKey());
+		tmp.putLong(certificate.getSerial());
+		tmp.putInt(certificate.getType().getCode());
+		tmp.putString(certificate.getId());
+		Buffer list = new ByteArrayBuffer();
+		list.putStringList(certificate.getPrincipals(), false);
+		tmp.putBytes(list.getCompactData());
+		tmp.putLong(certificate.getValidAfter());
+		tmp.putLong(certificate.getValidBefore());
+		tmp.putCertificateOptions(certificate.getCriticalOptions());
+		tmp.putCertificateOptions(certificate.getExtensions());
+		tmp.putString(certificate.getReserved());
+		Buffer inner = new ByteArrayBuffer();
+		inner.putRawPublicKey(certificate.getCaPubKey());
+		tmp.putBytes(inner.getCompactData());
+		return tmp.getCompactData();
+	}
+
+	/**
+	 * Checks whether a certificate is valid at a given time.
+	 *
+	 * @param certificate
+	 *            {@link OpenSshCertificate} to check
+	 * @param signatureTime
+	 *            {@link Instant} to check
+	 * @return {@code null} if the certificate is valid at the given instant;
+	 *         otherwise a descriptive message
+	 */
+	static String checkExpiration(OpenSshCertificate certificate,
+			@NonNull Instant signatureTime) {
+		long instant = signatureTime.getEpochSecond();
+		if (Long.compareUnsigned(instant, certificate.getValidAfter()) < 0) {
+			return MessageFormat.format(SshdText.get().signCertificateTooEarly,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+		} else if (Long.compareUnsigned(instant,
+				certificate.getValidBefore()) > 0) {
+			return MessageFormat.format(SshdText.get().signCertificateExpired,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+		}
+		return null;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java
new file mode 100644
index 0000000..bc72196
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java
@@ -0,0 +1,38 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jgit.lib.Constants;
+
+/**
+ * Defines common constants for SSH signatures.
+ */
+final class SshSignatureConstants {
+
+	private static final String SIGNATURE_END = "-----END SSH SIGNATURE-----"; //$NON-NLS-1$
+
+	static final byte[] MAGIC = { 'S', 'S', 'H', 'S', 'I', 'G' };
+
+	static final int VERSION = 1;
+
+	static final String NAMESPACE = "git"; //$NON-NLS-1$
+
+	static final byte[] ARMOR_HEAD = Constants.SSH_SIGNATURE_PREFIX
+			.getBytes(StandardCharsets.US_ASCII);
+
+	static final byte[] ARMOR_END = SIGNATURE_END
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private SshSignatureConstants() {
+		// No instantiation
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifier.java
new file mode 100644
index 0000000..76be340
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifier.java
@@ -0,0 +1,319 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Locale;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.signing.ssh.CachingSigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.SigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.VerificationException;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link SignatureVerifier} for SSH signatures.
+ */
+public class SshSignatureVerifier implements SignatureVerifier {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(SshSignatureVerifier.class);
+
+	private static final byte[] OBJECT = { 'o', 'b', 'j', 'e', 'c', 't', ' ' };
+
+	private static final byte[] TREE = { 't', 'r', 'e', 'e', ' ' };
+
+	private static final byte[] TYPE = { 't', 'y', 'p', 'e', ' ' };
+
+	@Override
+	public String getName() {
+		return "ssh"; //$NON-NLS-1$
+	}
+
+	@Override
+	public SignatureVerification verify(Repository repository, GpgConfig config,
+			byte[] data, byte[] signatureData) throws IOException {
+		// This is a bit stupid. SSH signatures do not store a signer, nor a
+		// time the signature was created. So we must use the committer's or
+		// tagger's PersonIdent, but here we have neither. But... if we see
+		// that the data is a commit or tag, then we can parse the PersonIdent
+		// from the data.
+		//
+		// Note: we cannot assume that absent a principal recorded in the
+		// allowedSignersFile or on a certificate that the key used to sign the
+		// commit belonged to the committer.
+		PersonIdent gitIdentity = getGitIdentity(data);
+		Date signatureDate = null;
+		Instant signatureInstant = null;
+		if (gitIdentity != null) {
+			signatureDate = gitIdentity.getWhen();
+			signatureInstant = gitIdentity.getWhenAsInstant();
+		}
+
+		TrustLevel trust = TrustLevel.NEVER;
+		byte[] decodedSignature;
+		try {
+			decodedSignature = dearmor(signatureData);
+		} catch (IllegalArgumentException e) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					null, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidSignature,
+							e.getLocalizedMessage()));
+		}
+		int start = RawParseUtils.match(decodedSignature, 0,
+				SshSignatureConstants.MAGIC);
+		if (start < 0) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					null, null, false, false, trust,
+					SshdText.get().signInvalidMagic);
+		}
+		ByteArrayBuffer signature = new ByteArrayBuffer(decodedSignature, start,
+				decodedSignature.length - start);
+
+		long version = signature.getUInt();
+		if (version != SshSignatureConstants.VERSION) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					null, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidVersion,
+							Long.toString(version)));
+		}
+
+		PublicKey key = signature.getPublicKey();
+		String fingerprint;
+		if (key instanceof OpenSshCertificate cert) {
+			fingerprint = KeyUtils.getFingerPrint(cert.getCertPubKey());
+			String message = SshCertificateUtils.verify(cert, signatureInstant);
+			if (message != null) {
+				return new SignatureVerification(getName(), signatureDate, null,
+						fingerprint, null, false, false, trust, message);
+			}
+		} else {
+			fingerprint = KeyUtils.getFingerPrint(key);
+		}
+
+		String namespace = signature.getString();
+		if (!SshSignatureConstants.NAMESPACE.equals(namespace)) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidNamespace,
+							namespace));
+		}
+
+		signature.getString(); // Skip the reserved field
+		String hashAlgorithm = signature.getString();
+		byte[] hash;
+		try {
+			hash = MessageDigest
+					.getInstance(hashAlgorithm.toUpperCase(Locale.ROOT))
+					.digest(data);
+		} catch (NoSuchAlgorithmException e) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(
+							SshdText.get().signUnknownHashAlgorithm,
+							hashAlgorithm));
+		}
+		ByteArrayBuffer rawSignature = new ByteArrayBuffer(
+				signature.getBytes());
+		if (signature.available() > 0) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					SshdText.get().signGarbageAtEnd);
+		}
+
+		String signatureAlgorithm = rawSignature.getString();
+		switch (signatureAlgorithm) {
+		case KeyPairProvider.SSH_DSS:
+		case KeyPairProvider.SSH_DSS_CERT:
+		case KeyPairProvider.SSH_RSA:
+		case KeyPairProvider.SSH_RSA_CERT:
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidAlgorithm,
+							signatureAlgorithm));
+		}
+
+		String keyType = KeyUtils
+				.getSignatureAlgorithm(KeyUtils.getKeyType(key), key);
+		if (!KeyUtils.getCanonicalKeyType(keyType)
+				.equals(KeyUtils.getCanonicalKeyType(signatureAlgorithm))) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(
+							SshdText.get().signMismatchedSignatureAlgorithm,
+							keyType, signatureAlgorithm));
+		}
+
+		BuiltinSignatures factory = BuiltinSignatures
+				.fromFactoryName(signatureAlgorithm);
+		if (factory == null || !factory.isSupported()) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(
+							SshdText.get().signUnknownSignatureAlgorithm,
+							signatureAlgorithm));
+		}
+
+		boolean valid;
+		String message = null;
+		try {
+			Signature verifier = factory.create();
+			verifier.initVerifier(null,
+					key instanceof OpenSshCertificate cert
+							? cert.getCertPubKey()
+							: key);
+			// Feed it the data
+			Buffer toSign = new ByteArrayBuffer();
+			toSign.putRawBytes(SshSignatureConstants.MAGIC);
+			toSign.putString(SshSignatureConstants.NAMESPACE);
+			toSign.putUInt(0); // reserved: zero-length string
+			toSign.putString(hashAlgorithm);
+			toSign.putBytes(hash);
+			verifier.update(null, toSign.getCompactData());
+			valid = verifier.verify(null, rawSignature.getBytes());
+		} catch (Exception e) {
+			LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+			valid = false;
+			message = SshdText.get().signSeeLog;
+		}
+		boolean expired = false;
+		String principal = null;
+		if (valid) {
+			if (rawSignature.available() > 0) {
+				valid = false;
+				message = SshdText.get().signGarbageAtEnd;
+			} else {
+				SigningKeyDatabase database = SigningKeyDatabase.getInstance();
+				if (database.isRevoked(repository, config, key)) {
+					valid = false;
+					if (key instanceof OpenSshCertificate certificate) {
+						message = MessageFormat.format(
+								SshdText.get().signCertificateRevoked,
+								KeyUtils.getFingerPrint(
+										certificate.getCaPubKey()));
+					} else {
+						message = SshdText.get().signKeyRevoked;
+					}
+				} else {
+					// This may turn a positive verification into a failed one.
+					try {
+						principal = database.isAllowed(repository, config, key,
+								SshSignatureConstants.NAMESPACE, gitIdentity);
+						if (!StringUtils.isEmptyOrNull(principal)) {
+							trust = TrustLevel.FULL;
+						} else {
+							valid = false;
+							message = SshdText.get().signNoPrincipalMatched;
+							trust = TrustLevel.UNKNOWN;
+						}
+					} catch (VerificationException e) {
+						valid = false;
+						message = e.getMessage();
+						expired = e.isExpired();
+					} catch (IOException e) {
+						LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+						valid = false;
+						message = SshdText.get().signSeeLog;
+					}
+				}
+			}
+		}
+		return new SignatureVerification(getName(), signatureDate, null,
+				fingerprint, principal, valid, expired, trust, message);
+	}
+
+	private static PersonIdent getGitIdentity(byte[] rawObject) {
+		// Data from a commit will start with "tree ID\n".
+		int i = RawParseUtils.match(rawObject, 0, TREE);
+		if (i > 0) {
+			i = RawParseUtils.committer(rawObject, 0);
+			if (i < 0) {
+				return null;
+			}
+			return RawParseUtils.parsePersonIdent(rawObject, i);
+		}
+		// Data from a tag will start with "object ID\ntype ".
+		i = RawParseUtils.match(rawObject, 0, OBJECT);
+		if (i > 0) {
+			i = RawParseUtils.nextLF(rawObject, i);
+			i = RawParseUtils.match(rawObject, i, TYPE);
+			if (i > 0) {
+				i = RawParseUtils.tagger(rawObject, 0);
+				if (i < 0) {
+					return null;
+				}
+				return RawParseUtils.parsePersonIdent(rawObject, i);
+			}
+		}
+		return null;
+	}
+
+	private static byte[] dearmor(byte[] data) {
+		int start = RawParseUtils.match(data, 0,
+				SshSignatureConstants.ARMOR_HEAD);
+		if (start > 0) {
+			if (data[start] == '\r') {
+				start++;
+			}
+			if (data[start] == '\n') {
+				start++;
+			}
+		}
+		int end = data.length;
+		if (end > start + 1 && data[end - 1] == '\n') {
+			end--;
+			if (end > start + 1 && data[end - 1] == '\r') {
+				end--;
+			}
+		}
+		end = end - SshSignatureConstants.ARMOR_END.length;
+		if (end >= 0 && end >= start
+				&& RawParseUtils.match(data, end,
+						SshSignatureConstants.ARMOR_END) >= 0) {
+			// end is fine: on the first the character of the end marker
+		} else {
+			// No end marker.
+			end = data.length;
+		}
+		if (start < 0) {
+			start = 0;
+		}
+		return Base64.decode(data, start, end - start);
+	}
+
+	@Override
+	public void clear() {
+		SigningKeyDatabase database = SigningKeyDatabase.getInstance();
+		if (database instanceof CachingSigningKeyDatabase caching) {
+			caching.clearCache();
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java
new file mode 100644
index 0000000..8cfe5f4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java
@@ -0,0 +1,485 @@
+/*
+ * 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.internal.signing.ssh;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StreamCorruptedException;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
+import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.internal.transport.sshd.agent.SshAgentClient;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProviderFactory;
+import org.eclipse.jgit.transport.sshd.agent.Connector;
+import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link Signer} to create SSH signatures.
+ *
+ * @see <a href=
+ *      "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig">PROTOCOL.sshsig</a>
+ */
+public class SshSigner implements Signer {
+
+	private static final Logger LOG = LoggerFactory.getLogger(SshSigner.class);
+
+	private static final String GIT_KEY_PREFIX = "key::"; //$NON-NLS-1$
+
+	// Base64 encoded lines should not be longer than 75 characters, plus the
+	// newline.
+	private static final int LINE_LENGTH = 75;
+
+	@Override
+	public GpgSignature sign(Repository repository, GpgConfig config,
+			byte[] data, PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException,
+			IOException, UnsupportedSigningFormatException {
+		byte[] hash;
+		try {
+			hash = MessageDigest.getInstance("SHA512").digest(data); //$NON-NLS-1$
+		} catch (NoSuchAlgorithmException e) {
+			throw new UnsupportedSigningFormatException(
+					MessageFormat.format(
+							SshdText.get().signUnknownHashAlgorithm, "SHA512"), //$NON-NLS-1$
+					e);
+		}
+		Buffer toSign = new ByteArrayBuffer();
+		toSign.putRawBytes(SshSignatureConstants.MAGIC);
+		toSign.putString(SshSignatureConstants.NAMESPACE);
+		toSign.putUInt(0); // reserved: zero-length string
+		toSign.putString("sha512"); //$NON-NLS-1$
+		toSign.putBytes(hash);
+		String key = signingKey;
+		if (StringUtils.isEmptyOrNull(key)) {
+			key = config.getSigningKey();
+		}
+		if (StringUtils.isEmptyOrNull(key)) {
+			key = defaultKeyCommand(repository, config);
+			// According to documentation, this is supposed to return a
+			// valid SSH public key prefixed with "key::". We don't enforce
+			// this: there might be older command implementations (like just
+			// calling "ssh-add -L") that return keys without prefix.
+		}
+		PublicKeyIdentity identity;
+		try {
+			identity = getIdentity(key, committer, credentialsProvider);
+		} catch (GeneralSecurityException e) {
+			throw new UnsupportedSigningFormatException(MessageFormat
+					.format(SshdText.get().signPublicKeyError, key), e);
+		}
+		String algorithm = KeyUtils
+				.getKeyType(identity.getKeyIdentity().getPublic());
+		switch (algorithm) {
+		case KeyPairProvider.SSH_DSS:
+		case KeyPairProvider.SSH_DSS_CERT:
+			throw new UnsupportedSigningFormatException(
+					SshdText.get().signInvalidKeyDSA);
+		case KeyPairProvider.SSH_RSA:
+			algorithm = KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS;
+			break;
+		case KeyPairProvider.SSH_RSA_CERT:
+			algorithm = KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS;
+			break;
+		default:
+			break;
+		}
+
+		Map.Entry<String, byte[]> rawSignature;
+		try {
+			rawSignature = identity.sign(null, algorithm,
+					toSign.getCompactData());
+		} catch (Exception e) {
+			throw new UnsupportedSigningFormatException(
+					SshdText.get().signSignatureError, e);
+		}
+		algorithm = rawSignature.getKey();
+		Buffer signature = new ByteArrayBuffer();
+		signature.putRawBytes(SshSignatureConstants.MAGIC);
+		signature.putUInt(SshSignatureConstants.VERSION);
+		signature.putPublicKey(identity.getKeyIdentity().getPublic());
+		signature.putString(SshSignatureConstants.NAMESPACE);
+		signature.putUInt(0); // reserved: zero-length string
+		signature.putString("sha512"); //$NON-NLS-1$
+		Buffer sig = new ByteArrayBuffer();
+		sig.putString(KeyUtils.getSignatureAlgorithm(algorithm,
+				identity.getKeyIdentity().getPublic()));
+		sig.putBytes(rawSignature.getValue());
+		signature.putBytes(sig.getCompactData());
+		return armor(signature.getCompactData());
+	}
+
+	private static String defaultKeyCommand(@NonNull Repository repository,
+			@NonNull GpgConfig config) throws IOException {
+		String command = config.getSshDefaultKeyCommand();
+		if (StringUtils.isEmptyOrNull(command)) {
+			return null;
+		}
+		FS fileSystem = repository.getFS();
+		if (fileSystem == null) {
+			fileSystem = FS.DETECTED;
+		}
+		ProcessBuilder builder = fileSystem.runInShell(command,
+				new String[] {});
+		ExecutionResult result = null;
+		try {
+			result = fileSystem.execute(builder, null);
+			int exitCode = result.getRc();
+			if (exitCode == 0) {
+				// The command is supposed to return a public key in its first
+				// line on stdout.
+				try (BufferedReader r = new BufferedReader(
+						new InputStreamReader(
+								result.getStdout().openInputStream(),
+								SystemReader.getInstance()
+										.getDefaultCharset()))) {
+					String line = r.readLine();
+					if (line != null) {
+						line = line.strip();
+					}
+					if (StringUtils.isEmptyOrNull(line)) {
+						throw new IOException(MessageFormat.format(
+								SshdText.get().signDefaultKeyEmpty, command));
+					}
+					return line;
+				}
+			}
+			TemporaryBuffer stderr = result.getStderr();
+			throw new IOException(MessageFormat.format(
+					SshdText.get().signDefaultKeyFailed, command,
+					Integer.toString(exitCode), toString(stderr)));
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new IOException(
+					MessageFormat.format(
+							SshdText.get().signDefaultKeyInterrupted, command),
+					e);
+		} finally {
+			if (result != null) {
+				if (result.getStderr() != null) {
+					result.getStderr().destroy();
+				}
+				if (result.getStdout() != null) {
+					result.getStdout().destroy();
+				}
+			}
+		}
+	}
+
+	private static String toString(TemporaryBuffer b) {
+		if (b != null) {
+			try {
+				return new String(b.toByteArray(4000),
+						SystemReader.getInstance().getDefaultCharset());
+			} catch (IOException e) {
+				LOG.warn("{}", SshdText.get().signStderr, e); //$NON-NLS-1$
+			}
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	private static PublicKeyIdentity getIdentity(String signingKey,
+			PersonIdent committer, CredentialsProvider credentials)
+			throws CanceledException, GeneralSecurityException, IOException {
+		if (StringUtils.isEmptyOrNull(signingKey)) {
+			throw new IllegalArgumentException(SshdText.get().signNoSigningKey);
+		}
+		PublicKey publicKey = null;
+		PrivateKey privateKey = null;
+		File keyFile = null;
+		if (signingKey.startsWith(GIT_KEY_PREFIX)) {
+			try (StringReader r = new StringReader(
+					signingKey.substring(GIT_KEY_PREFIX.length()))) {
+				publicKey = fromEntry(
+						AuthorizedKeyEntry.readAuthorizedKeys(r, true));
+			}
+		} else if (signingKey.startsWith("~/") //$NON-NLS-1$
+				|| signingKey.startsWith('~' + File.separator)) {
+			keyFile = new File(FS.DETECTED.userHome(), signingKey.substring(2));
+		} else {
+			try (StringReader r = new StringReader(signingKey)) {
+				publicKey = fromEntry(
+						AuthorizedKeyEntry.readAuthorizedKeys(r, true));
+			} catch (IOException e) {
+				// Ignore and try to read as a file
+				keyFile = new File(signingKey);
+			}
+		}
+		if (keyFile != null && keyFile.isFile()) {
+			try {
+				publicKey = fromEntry(AuthorizedKeyEntry
+						.readAuthorizedKeys(keyFile.toPath()));
+				if (publicKey == null) {
+					throw new IOException(MessageFormat.format(
+							SshdText.get().signTooManyPublicKeys, keyFile));
+				}
+				// Try to find the private key so we don't go looking for
+				// the agent (or PKCS#11) in vain.
+				keyFile = getPrivateKeyFile(keyFile.getParentFile(),
+						keyFile.getName());
+				if (keyFile != null) {
+					try {
+						KeyPair pair = loadPrivateKey(keyFile.toPath(),
+								credentials);
+						if (pair != null) {
+							PublicKey pk = pair.getPublic();
+							if (pk == null) {
+								privateKey = pair.getPrivate();
+							} else {
+								PublicKey original = publicKey;
+								if (publicKey instanceof OpenSshCertificate cert) {
+									original = cert.getCertPubKey();
+								}
+								if (KeyUtils.compareKeys(original, pk)) {
+									privateKey = pair.getPrivate();
+								}
+							}
+						}
+					} catch (IOException e) {
+						// Apparently it wasn't a private key file. Ignore.
+					}
+				}
+			} catch (StreamCorruptedException e) {
+				// File is readable, but apparently not a public key. Try to
+				// load it as a private key.
+				KeyPair pair = loadPrivateKey(keyFile.toPath(), credentials);
+				if (pair != null) {
+					publicKey = pair.getPublic();
+					privateKey = pair.getPrivate();
+				}
+			}
+		}
+		if (publicKey == null) {
+			throw new IOException(MessageFormat
+					.format(SshdText.get().signNoPublicKey, signingKey));
+		}
+		if (publicKey instanceof OpenSshCertificate cert) {
+			String message = SshCertificateUtils.verify(cert,
+					committer.getWhenAsInstant());
+			if (message != null) {
+				throw new IOException(message);
+			}
+		}
+		if (privateKey == null) {
+			// Could be in the agent, or a PKCS#11 key. The normal procedure
+			// with PKCS#11 keys is to put them in the agent and let the agent
+			// deal with it.
+			//
+			// This may or may not work well. For instance, the agent might ask
+			// for a passphrase for PKCS#11 keys... also, the OpenSSH ssh-agent
+			// had a bug with signing using PKCS#11 certificates in the agent;
+			// see https://bugzilla.mindrot.org/show_bug.cgi?id=3613 . If there
+			// are troubles, we might do the PKCS#11 dance ourselves, but we'd
+			// need additional configuration for the PKCS#11 library. (Plus
+			// some refactoring in the Pkcs11Provider.)
+			return new AgentIdentity(publicKey);
+
+		}
+		return new KeyPairIdentity(new KeyPair(publicKey, privateKey));
+	}
+
+	private static File getPrivateKeyFile(File directory,
+			String publicKeyName) {
+		if (publicKeyName.endsWith(".pub")) { //$NON-NLS-1$
+			String privateKeyName = publicKeyName.substring(0,
+					publicKeyName.length() - 4);
+			if (!privateKeyName.isEmpty()) {
+				File keyFile = new File(directory, privateKeyName);
+				if (keyFile.isFile()) {
+					return keyFile;
+				}
+				if (privateKeyName.endsWith("-cert")) { //$NON-NLS-1$
+					privateKeyName = privateKeyName.substring(0,
+							privateKeyName.length() - 5);
+					if (!privateKeyName.isEmpty()) {
+						keyFile = new File(directory, privateKeyName);
+						if (keyFile.isFile()) {
+							return keyFile;
+						}
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	private static KeyPair loadPrivateKey(Path path,
+			CredentialsProvider credentials)
+			throws CanceledException, GeneralSecurityException, IOException {
+		if (!Files.isRegularFile(path)) {
+			return null;
+		}
+		KeyPairResourceParser parser = SecurityUtils.getKeyPairResourceParser();
+		if (parser != null) {
+			PasswordProviderWrapper provider = null;
+			if (credentials != null) {
+				provider = new PasswordProviderWrapper(
+						() -> KeyPasswordProviderFactory.getInstance()
+								.apply(credentials));
+			}
+			try {
+				Collection<KeyPair> keyPairs = parser.loadKeyPairs(null, path,
+						provider);
+				if (keyPairs.size() != 1) {
+					throw new GeneralSecurityException(MessageFormat.format(
+							SshdText.get().signTooManyPrivateKeys, path));
+				}
+				return keyPairs.iterator().next();
+			} catch (AuthenticationCanceledException e) {
+				throw new CanceledException(e.getMessage());
+			}
+		}
+		return null;
+	}
+
+	private static GpgSignature armor(byte[] data) throws IOException {
+		try (ByteArrayOutputStream b = new ByteArrayOutputStream()) {
+			b.write(SshSignatureConstants.ARMOR_HEAD);
+			b.write('\n');
+			String encoded = Base64.encodeBytes(data);
+			int length = encoded.length();
+			int column = 0;
+			for (int i = 0; i < length; i++) {
+				b.write(encoded.charAt(i));
+				column++;
+				if (column == LINE_LENGTH) {
+					b.write('\n');
+					column = 0;
+				}
+			}
+			if (column > 0) {
+				b.write('\n');
+			}
+			b.write(SshSignatureConstants.ARMOR_END);
+			b.write('\n');
+			return new GpgSignature(b.toByteArray());
+		}
+	}
+
+	private static PublicKey fromEntry(List<AuthorizedKeyEntry> entries)
+			throws GeneralSecurityException, IOException {
+		if (entries == null || entries.size() != 1) {
+			return null;
+		}
+		return entries.get(0).resolvePublicKey(null,
+				PublicKeyEntryResolver.FAILING);
+	}
+
+	@Override
+	public boolean canLocateSigningKey(Repository repository, GpgConfig config,
+			PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException {
+		String key = signingKey;
+		if (key == null) {
+			key = config.getSigningKey();
+		}
+		return !(StringUtils.isEmptyOrNull(key)
+				&& StringUtils.isEmptyOrNull(config.getSshDefaultKeyCommand()));
+	}
+
+	private static class KeyPairIdentity implements PublicKeyIdentity {
+
+		private final @NonNull KeyPair pair;
+
+		KeyPairIdentity(@NonNull KeyPair pair) {
+			this.pair = pair;
+		}
+
+		@Override
+		public KeyPair getKeyIdentity() {
+			return pair;
+		}
+
+		@Override
+		public Entry<String, byte[]> sign(SessionContext session, String algo,
+				byte[] data) throws Exception {
+			BuiltinSignatures factory = BuiltinSignatures.fromFactoryName(algo);
+			if (factory == null || !factory.isSupported()) {
+				throw new GeneralSecurityException(MessageFormat.format(
+						SshdText.get().signUnknownSignatureAlgorithm, algo));
+			}
+			Signature signer = factory.create();
+			signer.initSigner(null, pair.getPrivate());
+			signer.update(null, data);
+			return new SimpleImmutableEntry<>(factory.getName(),
+					signer.sign(null));
+		}
+	}
+
+	private static class AgentIdentity extends KeyPairIdentity {
+
+		AgentIdentity(PublicKey publicKey) {
+			super(new KeyPair(publicKey, null));
+		}
+
+		@Override
+		public Entry<String, byte[]> sign(SessionContext session, String algo,
+				byte[] data) throws Exception {
+			ConnectorFactory factory = ConnectorFactory.getDefault();
+			Connector connector = factory == null ? null
+					: factory.create("", null); //$NON-NLS-1$
+			if (connector == null) {
+				throw new IOException(SshdText.get().signNoAgent);
+			}
+			try (SshAgentClient agent = new SshAgentClient(connector)) {
+				return agent.sign(null, getKeyIdentity().getPublic(), algo,
+						data);
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
index 2cd0669..900c9fb 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 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
@@ -47,6 +47,8 @@ private static class PerSessionState {
 
 	private final Supplier<KeyPasswordProvider> factory;
 
+	private PerSessionState noSessionState;
+
 	/**
 	 * Creates a new {@link PasswordProviderWrapper}.
 	 *
@@ -59,13 +61,18 @@ public PasswordProviderWrapper(
 	}
 
 	private PerSessionState getState(SessionContext context) {
-		PerSessionState state = context.getAttribute(STATE);
+		PerSessionState state = context != null ? context.getAttribute(STATE)
+				: noSessionState;
 		if (state == null) {
 			state = new PerSessionState();
 			state.delegate = factory.get();
 			state.delegate.setAttempts(
 					PASSWORD_PROMPTS.getRequiredDefault().intValue());
-			context.setAttribute(STATE, state);
+			if (context != null) {
+				context.setAttribute(STATE, state);
+			} else {
+				noSessionState = state;
+			}
 		}
 		return state;
 	}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
index 05f04ac..0533b65 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 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
@@ -147,6 +147,71 @@ public static SshdText get() {
 	/***/ public String sshCommandTimeout;
 	/***/ public String sshProcessStillRunning;
 	/***/ public String sshProxySessionCloseFailed;
+	/***/ public String signAllowedSignersCertAuthorityError;
+	/***/ public String signAllowedSignersEmptyIdentity;
+	/***/ public String signAllowedSignersEmptyNamespaces;
+	/***/ public String signAllowedSignersFormatError;
+	/***/ public String signAllowedSignersInvalidDate;
+	/***/ public String signAllowedSignersLineFormat;
+	/***/ public String signAllowedSignersMultiple;
+	/***/ public String signAllowedSignersNoIdentities;
+	/***/ public String signAllowedSignersPublicKeyParsing;
+	/***/ public String signAllowedSignersUnterminatedQuote;
+	/***/ public String signCertAlgorithmMismatch;
+	/***/ public String signCertAlgorithmUnknown;
+	/***/ public String signCertificateExpired;
+	/***/ public String signCertificateInvalid;
+	/***/ public String signCertificateNotForName;
+	/***/ public String signCertificateRevoked;
+	/***/ public String signCertificateTooEarly;
+	/***/ public String signCertificateWithoutPrincipals;
+	/***/ public String signDefaultKeyEmpty;
+	/***/ public String signDefaultKeyFailed;
+	/***/ public String signDefaultKeyInterrupted;
+	/***/ public String signGarbageAtEnd;
+	/***/ public String signInvalidAlgorithm;
+	/***/ public String signInvalidKeyDSA;
+	/***/ public String signInvalidMagic;
+	/***/ public String signInvalidNamespace;
+	/***/ public String signInvalidSignature;
+	/***/ public String signInvalidVersion;
+	/***/ public String signKeyExpired;
+	/***/ public String signKeyRevoked;
+	/***/ public String signKeyTooEarly;
+	/***/ public String signKrlBlobLeftover;
+	/***/ public String signKrlBlobLengthInvalid;
+	/***/ public String signKrlBlobLengthInvalidExpected;
+	/***/ public String signKrlCaKeyLengthInvalid;
+	/***/ public String signKrlCertificateLeftover;
+	/***/ public String signKrlCertificateSubsectionLeftover;
+	/***/ public String signKrlCertificateSubsectionLength;
+	/***/ public String signKrlEmptyRange;
+	/***/ public String signKrlInvalidBitSetLength;
+	/***/ public String signKrlInvalidKeyIdLength;
+	/***/ public String signKrlInvalidMagic;
+	/***/ public String signKrlInvalidReservedLength;
+	/***/ public String signKrlInvalidVersion;
+	/***/ public String signKrlNoCertificateSubsection;
+	/***/ public String signKrlSerialZero;
+	/***/ public String signKrlShortRange;
+	/***/ public String signKrlUnknownSection;
+	/***/ public String signKrlUnknownSubsection;
+	/***/ public String signLogFailure;
+	/***/ public String signMismatchedSignatureAlgorithm;
+	/***/ public String signNoAgent;
+	/***/ public String signNoPrincipalMatched;
+	/***/ public String signNoPublicKey;
+	/***/ public String signNoSigningKey;
+	/***/ public String signNotUserCertificate;
+	/***/ public String signPublicKeyError;
+	/***/ public String signSeeLog;
+	/***/ public String signSignatureError;
+	/***/ public String signStderr;
+	/***/ public String signTooManyPrivateKeys;
+	/***/ public String signTooManyPublicKeys;
+	/***/ public String signUnknownHashAlgorithm;
+	/***/ public String signUnknownSignatureAlgorithm;
+	/***/ public String signWrongNamespace;
 	/***/ public String unknownProxyProtocol;
 
 }
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/CachingSigningKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/CachingSigningKeyDatabase.java
new file mode 100644
index 0000000..4d2d8b6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/CachingSigningKeyDatabase.java
@@ -0,0 +1,49 @@
+/*
+ * 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.signing.ssh;
+
+/**
+ * A {@link SigningKeyDatabase} that caches data.
+ * <p>
+ * A signing key database may be used to check keys frequently; it may thus need
+ * to cache some data and it may need to cache data per repository. If an
+ * implementation does cache data, it is responsible itself for refreshing that
+ * cache at appropriate times. Clients can control the cache size somewhat via
+ * {@link #setCacheSize(int)}, although the meaning of the cache size (i.e., its
+ * unit) is left undefined here.
+ * </p>
+ *
+ * @since 7.1
+ */
+public interface CachingSigningKeyDatabase extends SigningKeyDatabase {
+
+	/**
+	 * Retrieves the current cache size.
+	 *
+	 * @return the cache size, or -1 if this database has no cache.
+	 */
+	int getCacheSize();
+
+	/**
+	 * Sets the cache size to use.
+	 *
+	 * @param size
+	 *            the cache size, ignored if this database does not have a
+	 *            cache.
+	 * @throws IllegalArgumentException
+	 *             if {@code size < 0}
+	 */
+	void setCacheSize(int size);
+
+	/**
+	 * Discards any cached data. A no-op if the database has no cache.
+	 */
+	void clearCache();
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SigningKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SigningKeyDatabase.java
new file mode 100644
index 0000000..eec64c3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SigningKeyDatabase.java
@@ -0,0 +1,94 @@
+/*
+ * 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.signing.ssh;
+
+import java.io.IOException;
+import java.security.PublicKey;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.signing.ssh.SigningDatabase;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * A database storing meta-information about signing keys and certificates.
+ *
+ * @since 7.1
+ */
+public interface SigningKeyDatabase {
+
+	/**
+	 * Obtains the current global instance.
+	 *
+	 * @return the global {@link SigningKeyDatabase}
+	 */
+	static SigningKeyDatabase getInstance() {
+		return SigningDatabase.getInstance();
+	}
+
+	/**
+	 * Sets the global {@link SigningKeyDatabase}.
+	 *
+	 * @param database
+	 *            to set; if {@code null} a default database using the OpenSSH
+	 *            allowed signers file and the OpenSSH revocation list mechanism
+	 *            is used.
+	 * @return the previously set {@link SigningKeyDatabase}
+	 */
+	static SigningKeyDatabase setInstance(SigningKeyDatabase database) {
+		return SigningDatabase.setInstance(database);
+	}
+
+	/**
+	 * Determines whether the gives key has been revoked.
+	 *
+	 * @param repository
+	 *            {@link Repository} the key is being used in
+	 * @param config
+	 *            {@link GpgConfig} to use
+	 * @param key
+	 *            {@link PublicKey} to check
+	 * @return {@code true} if the key has been revoked, {@code false} otherwise
+	 * @throws IOException
+	 *             if an I/O problem occurred
+	 */
+	boolean isRevoked(@NonNull Repository repository, @NonNull GpgConfig config,
+			@NonNull PublicKey key) throws IOException;
+
+	/**
+	 * Checks whether the given key is allowed to be used for signing, and if
+	 * allowed returns the principal.
+	 *
+	 * @param repository
+	 *            {@link Repository} the key is being used in
+	 * @param config
+	 *            {@link GpgConfig} to use
+	 * @param key
+	 *            {@link PublicKey} to check
+	 * @param namespace
+	 *            of the signature
+	 * @param ident
+	 *            optional {@link PersonIdent} giving a signer's e-mail address
+	 *            and a signature time
+	 * @return {@code null} if the database does not contain any information
+	 *         about the given key; the principal if it does and all checks
+	 *         passed
+	 * @throws IOException
+	 *             if an I/O problem occurred
+	 * @throws VerificationException
+	 *             if the database contains information about the key and the
+	 *             checks determined that the key is not allowed to be used for
+	 *             signing
+	 */
+	String isAllowed(@NonNull Repository repository, @NonNull GpgConfig config,
+			@NonNull PublicKey key, @NonNull String namespace,
+			PersonIdent ident) throws IOException, VerificationException;
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignatureVerifierFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignatureVerifierFactory.java
new file mode 100644
index 0000000..c315428
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignatureVerifierFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.signing.ssh;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.internal.signing.ssh.SshSignatureVerifier;
+import org.eclipse.jgit.lib.SignatureVerifierFactory;
+
+/**
+ * Factory creating {@link SshSignatureVerifier}s.
+ *
+ * @since 7.1
+ */
+public final class SshSignatureVerifierFactory
+		implements SignatureVerifierFactory {
+
+	@Override
+	public GpgFormat getType() {
+		return GpgFormat.SSH;
+	}
+
+	@Override
+	public SignatureVerifier create() {
+		return new SshSignatureVerifier();
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java
new file mode 100644
index 0000000..5459b53
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.signing.ssh;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.internal.signing.ssh.SshSigner;
+import org.eclipse.jgit.lib.SignerFactory;
+
+/**
+ * Factory creating {@link SshSigner}s.
+ *
+ * @since 7.1
+ */
+public final class SshSignerFactory implements SignerFactory {
+
+	@Override
+	public GpgFormat getType() {
+		return GpgFormat.SSH;
+	}
+
+	@Override
+	public Signer create() {
+		return new SshSigner();
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/VerificationException.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/VerificationException.java
new file mode 100644
index 0000000..cd77111
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/VerificationException.java
@@ -0,0 +1,63 @@
+/*
+ * 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.signing.ssh;
+
+/**
+ * An exception giving details about a failed
+ * {@link SigningKeyDatabase#isAllowed(org.eclipse.jgit.lib.Repository, org.eclipse.jgit.lib.GpgConfig, java.security.PublicKey, String, org.eclipse.jgit.lib.PersonIdent)}
+ * validation.
+ *
+ * @since 7.1
+ */
+public class VerificationException extends Exception {
+
+	private static final long serialVersionUID = 313760495170326160L;
+
+	private final boolean expired;
+
+	private final String reason;
+
+	/**
+	 * Creates a new instance.
+	 *
+	 * @param expired
+	 *            whether the checked public key or certificate was expired
+	 * @param reason
+	 *            describing the check failure
+	 */
+	public VerificationException(boolean expired, String reason) {
+		this.expired = expired;
+		this.reason = reason;
+	}
+
+	@Override
+	public String getMessage() {
+		return reason;
+	}
+
+	/**
+	 * Tells whether the check failed because the public key was expired.
+	 *
+	 * @return {@code true} if the check failed because the public key was
+	 *         expired, {@code false} otherwise
+	 */
+	public boolean isExpired() {
+		return expired;
+	}
+
+	/**
+	 * Retrieves the check failure reason.
+	 *
+	 * @return the reason description
+	 */
+	public String getReason() {
+		return reason;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProviderFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProviderFactory.java
new file mode 100644
index 0000000..0537300
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProviderFactory.java
@@ -0,0 +1,69 @@
+/*
+ * 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.transport.sshd;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * Maintains a static singleton instance of a factory to create a
+ * {@link KeyPasswordProvider} from a {@link CredentialsProvider}.
+ *
+ * @since 7.1
+ */
+public final class KeyPasswordProviderFactory {
+
+	/**
+	 * Creates a {@link KeyPasswordProvider} from a {@link CredentialsProvider}.
+	 */
+	@FunctionalInterface
+	public interface KeyPasswordProviderCreator
+			extends Function<CredentialsProvider, KeyPasswordProvider> {
+		// Nothing
+	}
+
+	private static final KeyPasswordProviderCreator DEFAULT = IdentityPasswordProvider::new;
+
+	private static AtomicReference<KeyPasswordProviderCreator> INSTANCE = new AtomicReference<>(
+			DEFAULT);
+
+	private KeyPasswordProviderFactory() {
+		// No instantiation
+	}
+
+	/**
+	 * Retrieves the currently set {@link KeyPasswordProviderCreator}.
+	 *
+	 * @return the {@link KeyPasswordProviderCreator}
+	 */
+	@NonNull
+	public static KeyPasswordProviderCreator getInstance() {
+		return INSTANCE.get();
+	}
+
+	/**
+	 * Sets a new {@link KeyPasswordProviderCreator}.
+	 *
+	 * @param provider
+	 *            to set; if {@code null}, sets a default provider.
+	 * @return the previously set {@link KeyPasswordProviderCreator}
+	 */
+	@NonNull
+	public static KeyPasswordProviderCreator setInstance(
+			KeyPasswordProviderCreator provider) {
+		if (provider == null) {
+			return INSTANCE.getAndSet(DEFAULT);
+		}
+		return INSTANCE.getAndSet(provider);
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
index 2c3cbe5..4a2eb9c 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 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
@@ -210,7 +210,7 @@ public SshdSession getSession(URIish uri,
 						home, sshDir);
 				KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
 						getDefaultKeys(sshDir));
-				Supplier<KeyPasswordProvider> keyPasswordProvider = () -> createKeyPasswordProvider(
+				Supplier<KeyPasswordProvider> keyPasswordProvider = newKeyPasswordProvider(
 						credentialsProvider);
 				SshClient client = ClientBuilder.builder()
 						.factory(JGitSshClient::new)
@@ -574,12 +574,24 @@ protected final KeyCache getKeyCache() {
 	 * @param provider
 	 *            the {@link CredentialsProvider} to delegate to for user
 	 *            interactions
-	 * @return a new {@link KeyPasswordProvider}
+	 * @return a new {@link KeyPasswordProvider}, or {@code null} to use the
+	 *         global {@link KeyPasswordProviderFactory}
 	 */
-	@NonNull
 	protected KeyPasswordProvider createKeyPasswordProvider(
 			CredentialsProvider provider) {
-		return new IdentityPasswordProvider(provider);
+		return null;
+	}
+
+	private Supplier<KeyPasswordProvider> newKeyPasswordProvider(
+			CredentialsProvider credentials) {
+		return () -> {
+			KeyPasswordProvider provider = createKeyPasswordProvider(
+					credentials);
+			if (provider != null) {
+				return provider;
+			}
+			return KeyPasswordProviderFactory.getInstance().apply(credentials);
+		};
 	}
 
 	/**
diff --git a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
index 8821702..71977c6 100644
--- a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
@@ -3,20 +3,20 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.jsch.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
 Import-Package: com.jcraft.jsch;version="[0.1.54,0.2.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit.ssh;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.ssh.jsch;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit.ssh;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.ssh.jsch;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
  org.hamcrest;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.ssh.jsch.test/pom.xml b/org.eclipse.jgit.ssh.jsch.test/pom.xml
index 4c9097a..f67b1bd 100644
--- a/org.eclipse.jgit.ssh.jsch.test/pom.xml
+++ b/org.eclipse.jgit.ssh.jsch.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch.test</artifactId>
diff --git a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
index 92840a9..3a6e08c 100644
--- a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
@@ -3,19 +3,19 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.jsch
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch;singleton:=true
-Fragment-Host: org.eclipse.jgit;bundle-version="[7.0.2,7.1.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[7.1.2,7.2.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/jsch
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.transport.ssh.jsch;version="7.0.2"
+Export-Package: org.eclipse.jgit.transport.ssh.jsch;version="7.1.2"
 Import-Package: com.jcraft.jsch;version="[0.1.37,0.2.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.io;version="[7.1.2,7.2.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
diff --git a/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF
index c0e1acc..f4c12e6 100644
--- a/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.jsch - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.jsch/pom.xml b/org.eclipse.jgit.ssh.jsch/pom.xml
index 1f4564c..4427602 100644
--- a/org.eclipse.jgit.ssh.jsch/pom.xml
+++ b/org.eclipse.jgit.ssh.jsch/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index eb95243..77a7f39 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.test
 Bundle-SymbolicName: org.eclipse.jgit.test
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -21,64 +21,64 @@
  org.apache.commons.io;version="[2.15.0,3.0.0)",
  org.apache.commons.io.output;version="[2.15.0,3.0.0)",
  org.assertj.core.api;version="[3.14.0,4.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.archive;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.attributes;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.awtui;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.blame;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.events;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.fnmatch;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.gitrepo;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.hooks;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.ignore;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.ignore.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.diff;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.fsck;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.commitgraph;version="7.0.2",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.io;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.memory;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.connectivity;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.parser;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.junit.time;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.logging;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.notes;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.patch;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.pgm;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.pgm.internal;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revplot;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.storage.pack;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.submodule;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util.sha1;version="[7.0.2,7.1.0)",
+ org.eclipse.jgit.annotations;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.api.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.archive;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.attributes;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.awtui;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.blame;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.diff;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.dircache;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.events;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.fnmatch;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.gitrepo;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.hooks;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.ignore;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.ignore.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.diff;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.fsck;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.commitgraph;version="7.1.2",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.io;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.memory;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.connectivity;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.junit.time;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lfs;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.logging;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.merge;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.notes;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.patch;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.pgm;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.pgm.internal;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revplot;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.file;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.storage.pack;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.submodule;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.http;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.io;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util.sha1;version="[7.1.2,7.2.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
  org.junit.function;version="[4.13.0,5.0.0)",
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index d44ca07..479050b 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.test</artifactId>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
index 12300b3..6d5e45c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
@@ -21,6 +21,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.Map;
 import java.util.concurrent.Callable;
 
 import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
@@ -29,6 +30,7 @@
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -117,6 +119,7 @@ public void testPullMerge() throws Exception {
 					+ db.getWorkTree().getAbsolutePath();
 			assertEquals(message, mergeCommit.getShortMessage());
 		}
+		assertTrue(target.status().call().isClean());
 	}
 
 	@Test
@@ -153,6 +156,10 @@ public void testPullConflict() throws Exception {
 		assertFileContentsEqual(targetFile, result);
 		assertEquals(RepositoryState.MERGING, target.getRepository()
 				.getRepositoryState());
+		Status status = target.status().call();
+		Map<String, StageState> conflicting = status.getConflictingStageState();
+		assertEquals(1, conflicting.size());
+		assertEquals(StageState.BOTH_MODIFIED, conflicting.get("SomeFile.txt"));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java
deleted file mode 100644
index d0fbdbd..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2019 Alex Jitianu <alex_jitianu@sync.ro> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.api;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.Policy;
-import java.util.Collections;
-
-import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.util.FileUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Tests that using a SecurityManager does not result in errors logged.
- */
-public class SecurityManagerMissingPermissionsTest extends RepositoryTestCase {
-
-	/**
-	 * Collects all logging sent to the logging system.
-	 */
-	private final ByteArrayOutputStream errorOutput = new ByteArrayOutputStream();
-
-	private SecurityManager originalSecurityManager;
-
-	private PrintStream defaultErrorOutput;
-
-	@Override
-	@Before
-	public void setUp() throws Exception {
-		originalSecurityManager = System.getSecurityManager();
-
-		// slf4j-simple logs to System.err, redirect it to enable asserting
-		// logged errors
-		defaultErrorOutput = System.err;
-		System.setErr(new PrintStream(errorOutput));
-
-		refreshPolicyAllPermission(Policy.getPolicy());
-		System.setSecurityManager(new SecurityManager());
-		super.setUp();
-	}
-
-	/**
-	 * If a SecurityManager is active a lot of {@link java.io.FilePermission}
-	 * errors are thrown and logged while initializing a repository.
-	 *
-	 * @throws Exception
-	 */
-	@Test
-	public void testCreateNewRepos_MissingPermissions() throws Exception {
-		File wcTree = new File(getTemporaryDirectory(),
-				"CreateNewRepositoryTest_testCreateNewRepos");
-
-		File marker = new File(getTemporaryDirectory(), "marker");
-		Files.write(marker.toPath(), Collections.singletonList("Can write"));
-		assertTrue("Can write in test directory", marker.isFile());
-		FileUtils.delete(marker);
-		assertFalse("Can delete in test direcory", marker.exists());
-
-		Git git = Git.init().setBare(false)
-				.setDirectory(new File(wcTree.getAbsolutePath())).call();
-
-		addRepoToClose(git.getRepository());
-
-		assertEquals("", errorOutput.toString());
-	}
-
-	@Override
-	@After
-	public void tearDown() throws Exception {
-		System.setSecurityManager(originalSecurityManager);
-		System.setErr(defaultErrorOutput);
-		super.tearDown();
-	}
-
-	/**
-	 * Refresh the Java Security Policy.
-	 *
-	 * @param policy
-	 *            the policy object
-	 *
-	 * @throws IOException
-	 *             if the temporary file that contains the policy could not be
-	 *             created
-	 */
-	private static void refreshPolicyAllPermission(Policy policy)
-			throws IOException {
-		// Starting with an all permissions policy.
-		String policyString = "grant { permission java.security.AllPermission; };";
-
-		// Do not use TemporaryFilesFactory, it will create a dependency cycle
-		Path policyFile = Files.createTempFile("testpolicy", ".txt");
-
-		try {
-			Files.write(policyFile, Collections.singletonList(policyString));
-			System.setProperty("java.security.policy",
-					policyFile.toUri().toURL().toString());
-			policy.refresh();
-		} finally {
-			try {
-				Files.delete(policyFile);
-			} catch (IOException e) {
-				// Do not log; the test tests for no logging having occurred
-				e.printStackTrace();
-			}
-		}
-	}
-
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java
deleted file mode 100644
index 2b930a1..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2019 Nail Samatov <sanail@yandex.ru> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.api;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.FilePermission;
-import java.io.IOException;
-import java.lang.reflect.ReflectPermission;
-import java.nio.file.Files;
-import java.security.Permission;
-import java.security.SecurityPermission;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.PropertyPermission;
-import java.util.logging.LoggingPermission;
-
-import javax.security.auth.AuthPermission;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.junit.JGitTestUtil;
-import org.eclipse.jgit.junit.MockSystemReader;
-import org.eclipse.jgit.junit.SeparateClassloaderTestRunner;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.FileUtils;
-import org.eclipse.jgit.util.SystemReader;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * <p>
- * Tests if jgit works if SecurityManager is enabled.
- * </p>
- *
- * <p>
- * Note: JGit's classes shouldn't be used before SecurityManager is configured.
- * If you use some JGit's class before SecurityManager is replaced then part of
- * the code can be invoked outside of our custom SecurityManager and this test
- * becomes useless.
- * </p>
- *
- * <p>
- * For example the class {@link org.eclipse.jgit.util.FS} is used widely in jgit
- * sources. It contains DETECTED static field. At the first usage of the class
- * FS the field DETECTED is initialized and during initialization many system
- * operations that SecurityManager can forbid are invoked.
- * </p>
- *
- * <p>
- * For this reason this test doesn't extend LocalDiskRepositoryTestCase (it uses
- * JGit's classes in setUp() method) and other JGit's utility classes. It's done
- * to affect SecurityManager as less as possible.
- * </p>
- *
- * <p>
- * We use SeparateClassloaderTestRunner to isolate FS.DETECTED field
- * initialization between different tests run.
- * </p>
- */
-@RunWith(SeparateClassloaderTestRunner.class)
-public class SecurityManagerTest {
-	private File root;
-
-	private SecurityManager originalSecurityManager;
-
-	private List<Permission> permissions = new ArrayList<>();
-
-	@Before
-	public void setUp() throws Exception {
-		// Create working directory
-		SystemReader.setInstance(new MockSystemReader());
-		root = Files.createTempDirectory("jgit-security").toFile();
-
-		// Add system permissions
-		permissions.add(new RuntimePermission("*"));
-		permissions.add(new SecurityPermission("*"));
-		permissions.add(new AuthPermission("*"));
-		permissions.add(new ReflectPermission("*"));
-		permissions.add(new PropertyPermission("*", "read,write"));
-		permissions.add(new LoggingPermission("control", null));
-
-		permissions.add(new FilePermission(
-				System.getProperty("java.home") + "/-", "read"));
-
-		String tempDir = System.getProperty("java.io.tmpdir");
-		permissions.add(new FilePermission(tempDir, "read,write,delete"));
-		permissions
-				.add(new FilePermission(tempDir + "/-", "read,write,delete"));
-
-		// Add permissions to dependent jar files.
-		String classPath = System.getProperty("java.class.path");
-		if (classPath != null) {
-			for (String path : classPath.split(File.pathSeparator)) {
-				permissions.add(new FilePermission(path, "read"));
-			}
-		}
-		// Add permissions to jgit class files.
-		String jgitSourcesRoot = new File(System.getProperty("user.dir"))
-				.getParent();
-		permissions.add(new FilePermission(jgitSourcesRoot + "/-", "read"));
-
-		// Add permissions to working dir for jgit. Our git repositories will be
-		// initialized and cloned here.
-		permissions.add(new FilePermission(root.getPath() + "/-",
-				"read,write,delete,execute"));
-
-		// Replace Security Manager
-		originalSecurityManager = System.getSecurityManager();
-		System.setSecurityManager(new SecurityManager() {
-
-			@Override
-			public void checkPermission(Permission requested) {
-				for (Permission permission : permissions) {
-					if (permission.implies(requested)) {
-						return;
-					}
-				}
-
-				super.checkPermission(requested);
-			}
-		});
-	}
-
-	@After
-	public void tearDown() throws Exception {
-		System.setSecurityManager(originalSecurityManager);
-
-		// Note: don't use this method before security manager is replaced in
-		// setUp() method. The method uses FS.DETECTED internally and can affect
-		// the test.
-		FileUtils.delete(root, FileUtils.RECURSIVE | FileUtils.RETRY);
-	}
-
-	@Test
-	public void testInitAndClone() throws IOException, GitAPIException {
-		File remote = new File(root, "remote");
-		File local = new File(root, "local");
-
-		try (Git git = Git.init().setDirectory(remote).call()) {
-			JGitTestUtil.write(new File(remote, "hello.txt"), "Hello world!");
-			git.add().addFilepattern(".").call();
-			git.commit().setMessage("Initial commit").call();
-		}
-
-		try (Git git = Git.cloneRepository().setURI(remote.toURI().toString())
-				.setDirectory(local).call()) {
-			assertTrue(new File(local, ".git").exists());
-
-			JGitTestUtil.write(new File(local, "hi.txt"), "Hi!");
-			git.add().addFilepattern(".").call();
-			RevCommit commit1 = git.commit().setMessage("Commit on local repo")
-					.call();
-			assertEquals("Commit on local repo", commit1.getFullMessage());
-			assertNotNull(TreeWalk.forPath(git.getRepository(), "hello.txt",
-					commit1.getTree()));
-		}
-
-	}
-
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java
new file mode 100644
index 0000000..2c4b432
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertArrayEquals;
+
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.junit.Test;
+
+public class AggregatedBlockCacheStatsTest {
+	@Test
+	public void getName() {
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of());
+
+		assertThat(aggregatedBlockCacheStats.getName(),
+				equalTo(AggregatedBlockCacheStats.class.getName()));
+	}
+
+	@Test
+	public void getCurrentSize_aggregatesCurrentSizes() {
+		long[] currentSizes = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5);
+		currentSizes[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6);
+		currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7);
+		currentSizes[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getCurrentSize(),
+				currentSizes);
+	}
+
+	@Test
+	public void getHitCount_aggregatesHitCounts() {
+		long[] hitCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+		hitCounts[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+		hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementHit(new TestKey(PackExt.INDEX)));
+		hitCounts[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getHitCount(), hitCounts);
+	}
+
+	@Test
+	public void getMissCount_aggregatesMissCounts() {
+		long[] missCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementMiss(new TestKey(PackExt.PACK)));
+		missCounts[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)));
+		missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+		missCounts[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getMissCount(), missCounts);
+	}
+
+	@Test
+	public void getTotalRequestCount_aggregatesRequestCounts() {
+		long[] totalRequestCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5, () -> {
+			packStats.incrementHit(new TestKey(PackExt.PACK));
+			packStats.incrementMiss(new TestKey(PackExt.PACK));
+		});
+		totalRequestCounts[PackExt.PACK.getPosition()] = 10;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> {
+			bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+			bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+		});
+		totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7, () -> {
+			indexStats.incrementHit(new TestKey(PackExt.INDEX));
+			indexStats.incrementMiss(new TestKey(PackExt.INDEX));
+		});
+		totalRequestCounts[PackExt.INDEX.getPosition()] = 14;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getTotalRequestCount(),
+				totalRequestCounts);
+	}
+
+	@Test
+	public void getHitRatio_aggregatesHitRatios() {
+		long[] hitRatios = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+		hitRatios[PackExt.PACK.getPosition()] = 100;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> {
+			bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+			bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+		});
+		hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+		hitRatios[PackExt.INDEX.getPosition()] = 0;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getHitRatio(), hitRatios);
+	}
+
+	@Test
+	public void getEvictions_aggregatesEvictions() {
+		long[] evictions = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementEvict(new TestKey(PackExt.PACK)));
+		evictions[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementEvict(new TestKey(PackExt.BITMAP_INDEX)));
+		evictions[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementEvict(new TestKey(PackExt.INDEX)));
+		evictions[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getEvictions(), evictions);
+	}
+
+	private static void incrementCounter(int amount, Runnable fn) {
+		for (int i = 0; i < amount; i++) {
+			fn.run();
+		}
+	}
+
+	private static long[] createEmptyStatsArray() {
+		return new long[PackExt.values().length];
+	}
+
+	private static class TestKey extends DfsStreamKey {
+		TestKey(PackExt packExt) {
+			super(0, packExt);
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			return false;
+		}
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java
new file mode 100644
index 0000000..2e2f86b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java
@@ -0,0 +1,67 @@
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME;
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.isA;
+
+import java.util.List;
+
+import org.junit.Test;
+
+public class ClockBlockCacheTableTest {
+	private static final String NAME = "name";
+
+	@Test
+	public void getName_nameNotConfigured_returnsDefaultName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig());
+
+		assertThat(cacheTable.getName(), equalTo(DEFAULT_NAME));
+	}
+
+	@Test
+	public void getName_nameConfigured_returnsConfiguredName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig().setName(NAME));
+
+		assertThat(cacheTable.getName(), equalTo(NAME));
+	}
+
+	@Test
+	public void getBlockCacheStats_nameNotConfigured_returnsBlockCacheStatsWithDefaultName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig());
+
+		assertThat(cacheTable.getBlockCacheStats(), hasSize(1));
+		assertThat(cacheTable.getBlockCacheStats().get(0).getName(),
+				equalTo(DEFAULT_NAME));
+	}
+
+	@Test
+	public void getBlockCacheStats_nameConfigured_returnsBlockCacheStatsWithConfiguredName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig().setName(NAME));
+
+		assertThat(cacheTable.getBlockCacheStats(), hasSize(1));
+		assertThat(cacheTable.getBlockCacheStats().get(0).getName(),
+				equalTo(NAME));
+	}
+
+	@Test
+	public void getAllBlockCacheStats() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig());
+
+		List<BlockCacheStats> blockCacheStats = cacheTable.getBlockCacheStats();
+		assertThat(blockCacheStats, contains(isA(BlockCacheStats.class)));
+	}
+
+	private static DfsBlockCacheConfig createBlockCacheConfig() {
+		return new DfsBlockCacheConfig().setBlockSize(512)
+				.setConcurrencyLevel(4).setBlockLimit(1024);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
index c93f48d..afa3179 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
@@ -38,6 +38,7 @@
 
 package org.eclipse.jgit.internal.storage.dfs;
 
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_CACHE_PREFIX;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
@@ -48,11 +49,17 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.closeTo;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThrows;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import org.eclipse.jgit.internal.JGitText;
@@ -173,6 +180,108 @@ public void fromConfig_generatesDfsBlockCachePackExtConfigs() {
 	}
 
 	@Test
+	public void fromConfig_withExistingCacheHotMap_configWithPackExtConfigsHasHotMaps() {
+		Config config = new Config();
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+				/* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+		addPackExtConfigEntry(config, "index",
+				List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+						PackExt.REVERSE_INDEX),
+				/* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+		Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1,
+				PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+		cacheConfig.setCacheHotMap(cacheHotMap);
+		cacheConfig.fromConfig(config);
+
+		var configs = cacheConfig.getPackExtCacheConfigurations();
+		assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap));
+		assertThat(configs, hasSize(3));
+		var packConfig = getConfigForExt(configs, PackExt.PACK);
+		assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1)));
+
+		var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+		assertThat(bitmapConfig.getCacheHotMap(),
+				is(Map.of(PackExt.BITMAP_INDEX, 2)));
+
+		var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+		assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3)));
+	}
+
+	@Test
+	public void setCacheHotMap_configWithPackExtConfigs_setsHotMaps() {
+		Config config = new Config();
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+				/* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+		addPackExtConfigEntry(config, "index",
+				List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+						PackExt.REVERSE_INDEX),
+				/* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+		Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1,
+				PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		cacheConfig.setCacheHotMap(cacheHotMap);
+
+		var configs = cacheConfig.getPackExtCacheConfigurations();
+		assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap));
+		assertThat(configs, hasSize(3));
+		var packConfig = getConfigForExt(configs, PackExt.PACK);
+		assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1)));
+
+		var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+		assertThat(bitmapConfig.getCacheHotMap(),
+				is(Map.of(PackExt.BITMAP_INDEX, 2)));
+
+		var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+		assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3)));
+	}
+
+	@Test
+	public void fromConfigs_baseConfigOnly_nameSetFromConfigDfsSubSection() {
+		Config config = new Config();
+
+		DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		assertThat(blockCacheConfig.getName(), equalTo(DEFAULT_NAME));
+	}
+
+	@Test
+	public void fromConfigs_namesSetFromConfigDfsCachePrefixSubSections() {
+		Config config = new Config();
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO, "0.5");
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name1",
+				CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name2",
+				CONFIG_KEY_PACK_EXTENSIONS, PackExt.BITMAP_INDEX.name());
+
+		DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		assertThat(blockCacheConfig.getName(), equalTo("dfs"));
+		assertThat(
+				blockCacheConfig.getPackExtCacheConfigurations().get(0)
+						.getPackExtCacheConfiguration().getName(),
+				equalTo("dfs.name1"));
+		assertThat(
+				blockCacheConfig.getPackExtCacheConfigurations().get(1)
+						.getPackExtCacheConfiguration().getName(),
+				equalTo("dfs.name2"));
+	}
+
+	@Test
 	public void fromConfigs_dfsBlockCachePackExtConfigWithDuplicateExtensions_throws() {
 		Config config = new Config();
 		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
@@ -216,6 +325,46 @@ public void fromConfigs_dfsBlockCachePackExtConfigWithUnknownExtensions_throws()
 				() -> new DfsBlockCacheConfig().fromConfig(config));
 	}
 
+	@Test
+	public void writeConfigurationDebug_writesConfigsToWriter()
+			throws Exception {
+		Config config = new Config();
+		config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_LIMIT, 50 * 1024);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_SIZE, 1024);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_CONCURRENCY_LEVEL, 3);
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO, "0.5");
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		Map<PackExt, Integer> hotmap = Map.of(PackExt.PACK, 10);
+		cacheConfig.setCacheHotMap(hotmap);
+
+		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+		cacheConfig.print(new PrintWriter(byteArrayOutputStream, true,
+				StandardCharsets.UTF_8));
+
+		String writenConfig = byteArrayOutputStream
+				.toString(StandardCharsets.UTF_8);
+
+		List<String> writenLines = Arrays.asList(writenConfig.split("\n"));
+		assertThat(writenLines,
+				equalTo(List.of("Name: dfs", "  BlockLimit: " + (50 * 1024),
+						"  BlockSize: 1024", "  StreamRatio: 0.5",
+						"  ConcurrencyLevel: 3",
+						"  CacheHotMapEntry: " + PackExt.PACK + " : " + 10,
+						"  Name: dfs.pack", "    BlockLimit: " + 20 * 512,
+						"    BlockSize: 512", "    StreamRatio: 0.3",
+						"    ConcurrencyLevel: 32",
+						"    CacheHotMapEntry: " + PackExt.PACK + " : " + 10,
+						"    PackExts: " + List.of(PackExt.PACK))));
+	}
+
 	private static void addPackExtConfigEntry(Config config, String configName,
 			List<PackExt> packExts, long blockLimit, int blockSize) {
 		String packExtConfigName = CONFIG_DFS_CACHE_PREFIX + configName;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
index fef0563..3c7cc07 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
@@ -13,20 +13,24 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.LongStream;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.LongStream;
 
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
 import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.IndexEventConsumer;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.junit.TestRepository;
@@ -39,14 +43,35 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class DfsBlockCacheTest {
 	@Rule
 	public TestName testName = new TestName();
+
 	private TestRng rng;
+
 	private DfsBlockCache cache;
+
 	private ExecutorService pool;
 
+	private enum CacheType {
+		SINGLE_TABLE_CLOCK_BLOCK_CACHE, EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE
+	}
+
+	@Parameters(name = "cache type: {0}")
+	public static Iterable<? extends Object> data() {
+		return Arrays.asList(CacheType.SINGLE_TABLE_CLOCK_BLOCK_CACHE,
+				CacheType.EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE);
+	}
+
+	@Parameter
+	public CacheType cacheType;
+
 	@Before
 	public void setUp() {
 		rng = new TestRng(testName.getMethodName());
@@ -448,8 +473,28 @@ private void resetCache() {
 	}
 
 	private void resetCache(int concurrencyLevel) {
-		DfsBlockCache.reconfigure(new DfsBlockCacheConfig().setBlockSize(512)
-				.setConcurrencyLevel(concurrencyLevel).setBlockLimit(1 << 20));
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.setBlockSize(512).setConcurrencyLevel(concurrencyLevel)
+				.setBlockLimit(1 << 20);
+		switch (cacheType) {
+		case SINGLE_TABLE_CLOCK_BLOCK_CACHE:
+			// SINGLE_TABLE_CLOCK_BLOCK_CACHE doesn't modify the config.
+			break;
+		case EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE:
+			List<DfsBlockCachePackExtConfig> packExtCacheConfigs = new ArrayList<>();
+			for (PackExt packExt : PackExt.values()) {
+				DfsBlockCacheConfig extCacheConfig = new DfsBlockCacheConfig()
+						.setBlockSize(512).setConcurrencyLevel(concurrencyLevel)
+						.setBlockLimit(1 << 20)
+						.setPackExtCacheConfigurations(packExtCacheConfigs);
+				packExtCacheConfigs.add(new DfsBlockCachePackExtConfig(
+						EnumSet.of(packExt), extCacheConfig));
+			}
+			cacheConfig.setPackExtCacheConfigurations(packExtCacheConfigs);
+			break;
+		}
+		assertNotNull(cacheConfig);
+		DfsBlockCache.reconfigure(cacheConfig);
 		cache = DfsBlockCache.getInstance();
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
index 2be11d3..f9fbfe8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
@@ -6,6 +6,7 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -15,8 +16,11 @@
 import static org.junit.Assert.fail;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph;
@@ -24,6 +28,7 @@
 import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndex;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.reftable.LogCursor;
 import org.eclipse.jgit.internal.storage.reftable.RefCursor;
 import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
 import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
@@ -37,6 +42,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevBlob;
@@ -44,6 +50,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.GitDateParser;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.After;
 import org.junit.Before;
@@ -1275,6 +1282,90 @@ public void bitmapIndexWrittenDuringGc() throws Exception {
 				bitmapIndex.getXorBitmapCount() > 0);
 	}
 
+	@Test
+	public void gitGCWithRefLogExpire() throws Exception {
+		String master = "refs/heads/master";
+		RevCommit commit0 = commit().message("0").create();
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update(master, commit1);
+		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+		gc.setReftableConfig(new ReftableConfig());
+		run(gc);
+		DfsPackDescription t1 = odb.newPack(INSERT);
+		Ref next = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE,
+				"refs/heads/next", commit0.copy());
+		long currentDay = new Date().getTime();
+		GregorianCalendar cal = new GregorianCalendar(SystemReader
+				.getInstance().getTimeZone(), SystemReader.getInstance()
+				.getLocale());
+		long ten_days_ago = GitDateParser.parse("10 days ago",cal,SystemReader.getInstance()
+				.getLocale()).getTime() ;
+		long twenty_days_ago = GitDateParser.parse("20 days ago",cal,SystemReader.getInstance()
+				.getLocale()).getTime() ;
+		long thirty_days_ago = GitDateParser.parse("30 days ago",cal,SystemReader.getInstance()
+				.getLocale()).getTime() ;;
+		long fifty_days_ago = GitDateParser.parse("50 days ago",cal,SystemReader.getInstance()
+				.getLocale()).getTime() ;
+		PersonIdent who2 = new PersonIdent("J.Author", "authemail", currentDay, -8 * 60);
+		PersonIdent who3 = new PersonIdent("J.Author", "authemail", ten_days_ago, -8 * 60);
+		PersonIdent who4 = new PersonIdent("J.Author", "authemail", twenty_days_ago, -8 * 60);
+		PersonIdent who5 = new PersonIdent("J.Author", "authemail", thirty_days_ago, -8 * 60);
+		PersonIdent who6 = new PersonIdent("J.Author", "authemail", fifty_days_ago, -8 * 60);
+
+		try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) {
+			ReftableWriter w = new ReftableWriter(out);
+			w.setMinUpdateIndex(42);
+			w.setMaxUpdateIndex(42);
+			w.begin();
+			w.sortAndWriteRefs(Collections.singleton(next));
+			w.writeLog("refs/heads/branch", 1, who2, ObjectId.zeroId(),id(2), "Branch Message");
+			w.writeLog("refs/heads/branch1", 2, who3, ObjectId.zeroId(),id(3), "Branch Message1");
+			w.writeLog("refs/heads/branch2", 2, who4, ObjectId.zeroId(),id(4), "Branch Message2");
+			w.writeLog("refs/heads/branch3", 2, who5, ObjectId.zeroId(),id(5), "Branch Message3");
+			w.writeLog("refs/heads/branch4", 2, who6, ObjectId.zeroId(),id(6), "Branch Message4");
+			w.finish();
+			t1.addFileExt(REFTABLE);
+			t1.setReftableStats(w.getStats());
+		}
+		odb.commitPack(Collections.singleton(t1), null);
+
+		gc = new DfsGarbageCollector(repo);
+		gc.setReftableConfig(new ReftableConfig());
+		// Expire ref log entries older than 30 days
+		gc.setRefLogExpire(Instant.ofEpochMilli(thirty_days_ago));
+		run(gc);
+
+		// Single GC pack present with all objects.
+		assertEquals(1, odb.getPacks().length);
+		DfsPackFile pack = odb.getPacks()[0];
+		DfsPackDescription desc = pack.getPackDescription();
+
+		DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc);
+		try (DfsReader ctx = odb.newReader();
+			 ReftableReader rr = table.open(ctx);
+			 RefCursor rc = rr.allRefs();
+			 LogCursor lc = rr.allLogs()) {
+			assertTrue(rc.next());
+			assertEquals(master, rc.getRef().getName());
+			assertEquals(commit1, rc.getRef().getObjectId());
+			assertTrue(rc.next());
+			assertEquals(next.getName(), rc.getRef().getName());
+			assertEquals(commit0, rc.getRef().getObjectId());
+			assertFalse(rc.next());
+			assertTrue(lc.next());
+			assertEquals(lc.getRefName(),"refs/heads/branch");
+			assertTrue(lc.next());
+			assertEquals(lc.getRefName(),"refs/heads/branch1");
+			assertTrue(lc.next());
+			assertEquals(lc.getRefName(),"refs/heads/branch2");
+			// Old entries are purged
+			assertFalse(lc.next());
+
+		}
+
+	}
+
+
 	private RevCommit commitChain(RevCommit parent, int length)
 			throws Exception {
 		for (int i = 0; i < length; i++) {
@@ -1364,4 +1455,12 @@ private int countPacks(PackSource source) throws IOException {
 		}
 		return cnt;
 	}
+	private static ObjectId id(int i) {
+		byte[] buf = new byte[OBJECT_ID_LENGTH];
+		buf[0] = (byte) (i & 0xff);
+		buf[1] = (byte) ((i >>> 8) & 0xff);
+		buf[2] = (byte) ((i >>> 16) & 0xff);
+		buf[3] = (byte) (i >>> 24);
+		return ObjectId.fromRaw(buf);
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
index c516e30..c3b6aa8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
@@ -12,13 +12,18 @@
 
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Optional;
 
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -98,6 +103,40 @@ public void testEstimateGcPackSizeWithAnExistingGcPack() throws Exception {
 				pack.getPackDescription().getEstimatedPackSize());
 	}
 
+	@Test
+	public void testObjectSizeIndexWritten() throws Exception {
+		writeObjectSizeIndex(repo, true);
+		RevCommit commit0 = commit().message("0").create();
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update("master", commit1);
+
+		compact();
+
+		Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks())
+				.filter(pack -> pack.getPackDescription()
+						.getPackSource() == COMPACT)
+				.findFirst();
+		assertTrue(compactPack.isPresent());
+		assertTrue(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX));
+	}
+
+	@Test
+	public void testObjectSizeIndexNotWritten() throws Exception {
+		writeObjectSizeIndex(repo, false);
+		RevCommit commit0 = commit().message("0").create();
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update("master", commit1);
+
+		compact();
+
+		Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks())
+				.filter(pack -> pack.getPackDescription()
+						.getPackSource() == COMPACT)
+				.findFirst();
+		assertTrue(compactPack.isPresent());
+		assertFalse(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX));
+	}
+
 	private TestRepository<InMemoryRepository>.CommitBuilder commit() {
 		return git.commit();
 	}
@@ -108,4 +147,9 @@ private void compact() throws IOException {
 		compactor.compact(null);
 		odb.clearCache();
 	}
+
+	private static void writeObjectSizeIndex(DfsRepository repo, boolean should) {
+		repo.getConfig().setInt(ConfigConstants.CONFIG_PACK_SECTION, null,
+				ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, should ? 0 : -1);
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
index bc851f8..9680019 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
@@ -41,6 +41,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Before;
 import org.junit.Test;
@@ -309,7 +310,7 @@ private ObjectId setupPack(int bs, int ps) throws IOException {
 
 	private void assertPackSize() throws IOException {
 		try (DfsReader ctx = db.getObjectDatabase().newReader();
-				PackWriter pw = new PackWriter(ctx);
+		     PackWriter pw = new PackWriter(new PackConfig(), ctx);
 				ByteArrayOutputStream os = new ByteArrayOutputStream();
 				PackOutputStream out = new PackOutputStream(
 						NullProgressMonitor.INSTANCE, os, pw)) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
index 8c003e0..e7627bc 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
@@ -10,7 +10,10 @@
 
 package org.eclipse.jgit.internal.storage.dfs;
 
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.sameInstance;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertNotNull;
@@ -30,13 +33,14 @@
 import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref;
 import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader;
 import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
-import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.DfsBlockCacheStats;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.junit.Test;
 import org.mockito.Mockito;
 
 @SuppressWarnings({ "boxing", "unchecked" })
 public class PackExtBlockCacheTableTest {
+	private static final String CACHE_NAME = "CacheName";
+
 	@Test
 	public void fromBlockCacheConfigs_createsDfsPackExtBlockCacheTables() {
 		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
@@ -385,6 +389,65 @@ public void get_packExtDoesNotMapToCacheTable_callsDefaultCache() {
 	}
 
 	@Test
+	public void getName() {
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				cacheTableWithStats(/* name= */ "defaultName", packStats),
+				Map.of(PackExt.PACK, cacheTableWithStats(/* name= */ "packName",
+						packStats)));
+
+		assertThat(tables.getName(), equalTo("defaultName,packName"));
+	}
+
+	@Test
+	public void getAllBlockCacheStats() {
+		String defaultTableName = "default table";
+		DfsBlockCacheStats defaultStats = new DfsBlockCacheStats(
+				defaultTableName);
+		incrementCounter(4,
+				() -> defaultStats.incrementHit(new TestKey(PackExt.REFTABLE)));
+
+		String packTableName = "pack table";
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats(packTableName);
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+
+		String bitmapTableName = "bitmap table";
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(
+				bitmapTableName);
+		incrementCounter(6, () -> bitmapStats
+				.incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+
+		DfsBlockCacheTable defaultTable = cacheTableWithStats(defaultStats);
+		DfsBlockCacheTable packTable = cacheTableWithStats(packStats);
+		DfsBlockCacheTable bitmapTable = cacheTableWithStats(bitmapStats);
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(defaultTable, Map.of(PackExt.PACK, packTable,
+						PackExt.BITMAP_INDEX, bitmapTable));
+
+		List<BlockCacheStats> statsList = tables.getBlockCacheStats();
+		assertThat(statsList, hasSize(3));
+
+		long[] defaultTableHitCounts = createEmptyStatsArray();
+		defaultTableHitCounts[PackExt.REFTABLE.getPosition()] = 4;
+		assertArrayEquals(
+				getCacheStatsByName(statsList, defaultTableName).getHitCount(),
+				defaultTableHitCounts);
+
+		long[] packTableHitCounts = createEmptyStatsArray();
+		packTableHitCounts[PackExt.PACK.getPosition()] = 5;
+		assertArrayEquals(
+				getCacheStatsByName(statsList, packTableName).getHitCount(),
+				packTableHitCounts);
+
+		long[] bitmapHitCounts = createEmptyStatsArray();
+		bitmapHitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+		assertArrayEquals(
+				getCacheStatsByName(statsList, bitmapTableName).getHitCount(),
+				bitmapHitCounts);
+	}
+
+	@Test
 	public void getBlockCacheStats_getCurrentSize_consolidatesAllTableCurrentSizes() {
 		long[] currentSizes = createEmptyStatsArray();
 
@@ -406,7 +469,8 @@ public void getBlockCacheStats_getCurrentSize_consolidatesAllTableCurrentSizes()
 								cacheTableWithStats(bitmapStats), PackExt.INDEX,
 								cacheTableWithStats(indexStats)));
 
-		assertArrayEquals(tables.getBlockCacheStats().getCurrentSize(),
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getCurrentSize(),
 				currentSizes);
 	}
 
@@ -435,7 +499,9 @@ public void getBlockCacheStats_GetHitCount_consolidatesAllTableHitCounts() {
 								cacheTableWithStats(bitmapStats), PackExt.INDEX,
 								cacheTableWithStats(indexStats)));
 
-		assertArrayEquals(tables.getBlockCacheStats().getHitCount(), hitCounts);
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getHitCount(),
+				hitCounts);
 	}
 
 	@Test
@@ -463,7 +529,8 @@ public void getBlockCacheStats_getMissCount_consolidatesAllTableMissCounts() {
 								cacheTableWithStats(bitmapStats), PackExt.INDEX,
 								cacheTableWithStats(indexStats)));
 
-		assertArrayEquals(tables.getBlockCacheStats().getMissCount(),
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getMissCount(),
 				missCounts);
 	}
 
@@ -498,8 +565,9 @@ public void getBlockCacheStats_getTotalRequestCount_consolidatesAllTableTotalReq
 								cacheTableWithStats(bitmapStats), PackExt.INDEX,
 								cacheTableWithStats(indexStats)));
 
-		assertArrayEquals(tables.getBlockCacheStats().getTotalRequestCount(),
-				totalRequestCounts);
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats())
+				.getTotalRequestCount(), totalRequestCounts);
 	}
 
 	@Test
@@ -529,7 +597,9 @@ public void getBlockCacheStats_getHitRatio_consolidatesAllTableHitRatios() {
 								cacheTableWithStats(bitmapStats), PackExt.INDEX,
 								cacheTableWithStats(indexStats)));
 
-		assertArrayEquals(tables.getBlockCacheStats().getHitRatio(), hitRatios);
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getHitRatio(),
+				hitRatios);
 	}
 
 	@Test
@@ -557,10 +627,21 @@ public void getBlockCacheStats_getEvictions_consolidatesAllTableEvictions() {
 								cacheTableWithStats(bitmapStats), PackExt.INDEX,
 								cacheTableWithStats(indexStats)));
 
-		assertArrayEquals(tables.getBlockCacheStats().getEvictions(),
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getEvictions(),
 				evictions);
 	}
 
+	private BlockCacheStats getCacheStatsByName(
+			List<BlockCacheStats> blockCacheStats, String name) {
+		for (BlockCacheStats entry : blockCacheStats) {
+			if (entry.getName().equals(name)) {
+				return entry;
+			}
+		}
+		return null;
+	}
+
 	private static void incrementCounter(int amount, Runnable fn) {
 		for (int i = 0; i < amount; i++) {
 			fn.run();
@@ -572,9 +653,16 @@ private static void incrementCounter(int amount, Runnable fn) {
 	}
 
 	private static DfsBlockCacheTable cacheTableWithStats(
-			DfsBlockCacheStats dfsBlockCacheStats) {
+			BlockCacheStats dfsBlockCacheStats) {
+		return cacheTableWithStats(CACHE_NAME, dfsBlockCacheStats);
+	}
+
+	private static DfsBlockCacheTable cacheTableWithStats(String name,
+			BlockCacheStats dfsBlockCacheStats) {
 		DfsBlockCacheTable cacheTable = mock(DfsBlockCacheTable.class);
-		when(cacheTable.getBlockCacheStats()).thenReturn(dfsBlockCacheStats);
+		when(cacheTable.getName()).thenReturn(name);
+		when(cacheTable.getBlockCacheStats())
+				.thenReturn(List.of(dfsBlockCacheStats));
 		return cacheTable;
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
index bd36337..41a33df 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
@@ -29,6 +29,7 @@
 
 import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java
similarity index 99%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java
index ad2c891..cd73c6a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java
@@ -66,7 +66,7 @@
 import org.junit.Test;
 import org.mockito.Mockito;
 
-public class PackWriterTest extends SampleDataRepositoryTestCase {
+public class BasePackWriterTest extends SampleDataRepositoryTestCase {
 
 	private static final List<RevObject> EMPTY_LIST_REVS = Collections
 			.<RevObject> emptyList();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java
new file mode 100644
index 0000000..cd1264e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class GcNumberOfPackFilesSinceBitmapStatisticsTest extends GcTestCase {
+	@Test
+	public void testShouldReportZeroObjectsForInitializedRepo()
+			throws IOException {
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
+			throws Exception {
+		tr.packAndPrune();
+		long result = gc.getStatistics().numberOfPackFilesSinceBitmap;
+
+		assertEquals(repo.getObjectDatabase().getPacks().size(), result);
+	}
+
+	@Test
+	public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
+		// given
+		addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & gc
+		parent = addCommit(parent);
+		gc.gc().get();
+		assertEquals(2L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	private RevCommit addCommit(RevCommit parent) throws Exception {
+		PersonIdent ident = new PersonIdent("repo-metrics", "repo@metrics.com");
+		TestRepository<FileRepository>.CommitBuilder builder = tr.commit()
+				.author(ident);
+		if (parent != null) {
+			builder.parent(parent);
+		}
+		RevCommit commit = builder.create();
+		tr.update("master", commit);
+		parent = commit;
+		return parent;
+	}
+
+	private long repositoryBitmapFiles() throws IOException {
+		return StreamSupport
+				.stream(Files
+						.newDirectoryStream(repo.getObjectDatabase()
+								.getPackDirectory().toPath(), "pack-*.bitmap")
+						.spliterator(), false)
+				.count();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
index c572955..f84be21 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertSame;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.concurrent.BrokenBarrierException;
@@ -31,6 +30,8 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
@@ -49,7 +50,7 @@ public void looseRefPacked() throws Exception {
 		RevBlob a = tr.blob("a");
 		tr.lightweightTag("t", a);
 
-		gc.packRefs();
+		packRefs(false);
 		assertSame(repo.exactRef("refs/tags/t").getStorage(), Storage.PACKED);
 	}
 
@@ -60,7 +61,7 @@ public void emptyRefDirectoryDeleted() throws Exception {
 		String name = repo.findRef(ref).getName();
 		Path dir = repo.getCommonDirectory().toPath().resolve(name).getParent();
 		assertNotNull(dir);
-		gc.packRefs();
+		packRefs(true);
 		assertFalse(Files.exists(dir));
 	}
 
@@ -75,9 +76,9 @@ public void concurrentOnlyOneWritesPackedRefs() throws Exception {
 		Callable<Integer> packRefs = () -> {
 			syncPoint.await();
 			try {
-				gc.packRefs();
+				packRefs(false);
 				return 0;
-			} catch (IOException e) {
+			} catch (GitAPIException e) {
 				return 1;
 			}
 		};
@@ -102,7 +103,7 @@ public void whileRefLockedRefNotPackedNoError()
 				"refs/tags/t1"));
 		try {
 			refLock.lock();
-			gc.packRefs();
+			packRefs(false);
 		} finally {
 			refLock.unlock();
 		}
@@ -145,7 +146,7 @@ public boolean isForceUpdate() {
 
 			Future<Result> result2 = pool.submit(() -> {
 				refUpdateLockedRef.await();
-				gc.packRefs();
+				packRefs(false);
 				packRefsDone.await();
 				return null;
 			});
@@ -173,19 +174,20 @@ public void dontPackHEAD_nonBare() throws Exception {
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
-		gc.packRefs();
+		PackRefsCommand packRefsCommand = git.packRefs().setAll(true);
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
 
 		git.checkout().setName("refs/heads/side").call();
-		gc.packRefs();
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 
 		// check for detached HEAD
 		git.checkout().setName(first.getName()).call();
-		gc.packRefs();
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 	}
 
@@ -208,7 +210,7 @@ public void dontPackHEAD_bare() throws Exception {
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
-		gc.packRefs();
+		packRefs(true);
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
@@ -216,9 +218,14 @@ public void dontPackHEAD_bare() throws Exception {
 
 		// check for non-detached HEAD
 		repo.updateRef(Constants.HEAD).link("refs/heads/side");
-		gc.packRefs();
+		packRefs(true);
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getObjectId(),
 				second.getId());
 	}
+
+	private void packRefs(boolean all) throws GitAPIException {
+		new PackRefsCommand(repo).setAll(all).call();
+	}
+
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index 49e8a7b..e067beb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -28,6 +28,7 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.time.Instant;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -374,8 +375,10 @@ public void test008_FailOnWrongVersion() throws IOException {
 	public void test009_CreateCommitOldFormat() throws IOException {
 		final ObjectId treeId = insertTree(new TreeFormatter());
 		final CommitBuilder c = new CommitBuilder();
-		c.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c.setMessage("A Commit\n");
 		c.setTreeId(treeId);
 		assertEquals(treeId, c.getTreeId());
@@ -411,7 +414,8 @@ public void test020_createBlobTag() throws IOException {
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(emptyId, Constants.OBJ_BLOB);
 		t.setTag("test020");
-		t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+		t.setTagger(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		t.setMessage("test020 tagged\n");
 		ObjectId actid = insertTag(t);
 		assertEquals("6759556b09fbb4fd8ae5e315134481cc25d46954", actid.name());
@@ -419,8 +423,9 @@ public void test020_createBlobTag() throws IOException {
 		RevTag mapTag = parseTag(actid);
 		assertEquals(Constants.OBJ_BLOB, mapTag.getObject().getType());
 		assertEquals("test020 tagged\n", mapTag.getFullMessage());
-		assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
-				.getTaggerIdent());
+		assertEquals(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+				mapTag.getTaggerIdent());
 		assertEquals("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", mapTag
 				.getObject().getId().name());
 	}
@@ -434,7 +439,8 @@ public void test021_createTreeTag() throws IOException {
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE);
 		t.setTag("test021");
-		t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+		t.setTagger(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		t.setMessage("test021 tagged\n");
 		ObjectId actid = insertTag(t);
 		assertEquals("b0517bc8dbe2096b419d42424cd7030733f4abe5", actid.name());
@@ -442,8 +448,9 @@ public void test021_createTreeTag() throws IOException {
 		RevTag mapTag = parseTag(actid);
 		assertEquals(Constants.OBJ_TREE, mapTag.getObject().getType());
 		assertEquals("test021 tagged\n", mapTag.getFullMessage());
-		assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
-				.getTaggerIdent());
+		assertEquals(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+				mapTag.getTaggerIdent());
 		assertEquals("417c01c8795a35b8e835113a85a5c0c1c77f67fb", mapTag
 				.getObject().getId().name());
 	}
@@ -455,17 +462,18 @@ public void test022_createCommitTag() throws IOException {
 		almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
 		final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
 		final CommitBuilder almostEmptyCommit = new CommitBuilder();
-		almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L,
-				-2 * 60)); // not exactly the same
-		almostEmptyCommit.setCommitter(new PersonIdent(author, 1154236443000L,
-				-2 * 60));
+		almostEmptyCommit.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2)));
+		almostEmptyCommit.setCommitter(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2)));
 		almostEmptyCommit.setMessage("test022\n");
 		almostEmptyCommit.setTreeId(almostEmptyTreeId);
 		ObjectId almostEmptyCommitId = insertCommit(almostEmptyCommit);
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(almostEmptyCommitId, Constants.OBJ_COMMIT);
 		t.setTag("test022");
-		t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+		t.setTagger(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		t.setMessage("test022 tagged\n");
 		ObjectId actid = insertTag(t);
 		assertEquals("0ce2ebdb36076ef0b38adbe077a07d43b43e3807", actid.name());
@@ -473,8 +481,9 @@ public void test022_createCommitTag() throws IOException {
 		RevTag mapTag = parseTag(actid);
 		assertEquals(Constants.OBJ_COMMIT, mapTag.getObject().getType());
 		assertEquals("test022 tagged\n", mapTag.getFullMessage());
-		assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
-				.getTaggerIdent());
+		assertEquals(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+				mapTag.getTaggerIdent());
 		assertEquals("b5d3b45a96b340441f5abb9080411705c51cc86c", mapTag
 				.getObject().getId().name());
 	}
@@ -488,9 +497,9 @@ public void test023_createCommitNonAnullii() throws IOException {
 		CommitBuilder commit = new CommitBuilder();
 		commit.setTreeId(almostEmptyTreeId);
 		commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setEncoding(UTF_8);
 		commit.setMessage("\u00dcbergeeks");
 		ObjectId cid = insertCommit(commit);
@@ -509,9 +518,9 @@ public void test024_createCommitNonAscii() throws IOException {
 		CommitBuilder commit = new CommitBuilder();
 		commit.setTreeId(almostEmptyTreeId);
 		commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setEncoding(ISO_8859_1);
 		commit.setMessage("\u00dcbergeeks");
 		ObjectId cid = insertCommit(commit);
@@ -544,8 +553,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 				.fromString("00b1f73724f493096d1ffa0b0f1f1482dbb8c936"), treeId);
 
 		final CommitBuilder c1 = new CommitBuilder();
-		c1.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c1.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c1.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c1.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c1.setMessage("A Commit\n");
 		c1.setTreeId(treeId);
 		assertEquals(treeId, c1.getTreeId());
@@ -555,8 +566,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 		assertEquals(cmtid1, actid1);
 
 		final CommitBuilder c2 = new CommitBuilder();
-		c2.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c2.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c2.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c2.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c2.setMessage("A Commit 2\n");
 		c2.setTreeId(treeId);
 		assertEquals(treeId, c2.getTreeId());
@@ -577,8 +590,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 		assertEquals(actid1, rm2.getParent(0));
 
 		final CommitBuilder c3 = new CommitBuilder();
-		c3.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c3.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c3.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c3.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c3.setMessage("A Commit 3\n");
 		c3.setTreeId(treeId);
 		assertEquals(treeId, c3.getTreeId());
@@ -600,8 +615,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 		assertEquals(actid2, rm3.getParent(1));
 
 		final CommitBuilder c4 = new CommitBuilder();
-		c4.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c4.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c4.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c4.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c4.setMessage("A Commit 4\n");
 		c4.setTreeId(treeId);
 		assertEquals(treeId, c3.getTreeId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
index 97da175..943a68b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
@@ -55,7 +55,8 @@ public void testNewIdentInstant() {
 				p.getWhenAsInstant());
 		assertEquals("A U Thor <author@example.com> 1142878501 -0500",
 				p.toExternalString());
-		assertEquals(ZoneId.of("GMT-05:00"), p.getZoneId());
+		assertEquals(ZoneId.of("GMT-05:00").getRules().getOffset(
+				Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset());
 	}
 
 	@Test
@@ -69,7 +70,8 @@ public void testNewIdentInstant2() {
 				p.getWhenAsInstant());
 		assertEquals("A U Thor <author@example.com> 1142878501 +0530",
 				p.toExternalString());
-		assertEquals(ZoneId.of("GMT+05:30"), p.getZoneId());
+		assertEquals(ZoneId.of("GMT+05:30").getRules().getOffset(
+				Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset());
 	}
 
 	@SuppressWarnings("unused")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
index 3a036ac..c6a6321 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -1792,7 +1792,77 @@ public void checkModeMergeConflictInVirtualAncestor(MergeStrategy strategy) thro
 		// children
 		mergeResult = git.merge().include(commitC3S).call();
 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
+	}
 
+	/**
+	 * Merging two commits when binary files have equal content, but conflicting content in the
+	 * virtual ancestor.
+	 *
+	 * <p>
+	 * This test has the same set up as
+	 * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only
+	 * with the content conflict in A1 and A2.
+	 */
+	@Theory
+	public void checkBinaryMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception {
+		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+			return;
+		}
+
+		Git git = Git.wrap(db);
+
+		// master
+		writeTrashFile("c", "initial file");
+		git.add().addFilepattern("c").call();
+		RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+		writeTrashFile("a", "\0\1\1\1\1\0");  // content in Ancestor 1
+		git.add().addFilepattern("a").call();
+		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+		writeTrashFile("a", "\0\1\2\3\4\5\0");  // content in Child 1 (commited on master)
+		git.add().addFilepattern("a").call();
+		// commit C1M
+		git.commit().setMessage("Child 1 on master").call();
+
+		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+		writeTrashFile("a", "\0\2\2\2\2\0");  // content in Ancestor 1
+		git.add().addFilepattern("a").call();
+		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+		// second branch
+		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+		writeTrashFile("a", "\0\5\4\3\2\1\0");  // content in Child 2 (commited on second-branch)
+		git.add().addFilepattern("a").call();
+		// commit C2S
+		git.commit().setMessage("Child 2 on second-branch").call();
+
+		// Merge branch-to-merge into second-branch
+		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+		// Resolve the conflict manually
+		writeTrashFile("a", "\0\3\3\3\3\0");  // merge conflict resolution
+		git.add().addFilepattern("a").call();
+		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
+
+		// Merge branch-to-merge into master
+		git.checkout().setName("master").call();
+		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+
+		// Resolve the conflict manually - set the same value as in resolution above
+		writeTrashFile("a", "\0\3\3\3\3\0");  // merge conflict resolution
+		git.add().addFilepattern("a").call();
+		// commit C4M
+		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
+
+		// Merge C4M (second-branch) into master (C3S)
+		// Conflict in virtual base should be here, but there are no conflicts in
+		// children
+		mergeResult = git.merge().include(commitC3S).call();
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
 	}
 
 	/**
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
index 2bd4003..aaecfd2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
@@ -2862,7 +2862,7 @@ public void testSingleBranchCloneTagChain() throws Exception {
 		RevTag heavyTag2 = remote.tag("middleTagRing", heavyTag1);
 		remote.lightweightTag("refTagRing", heavyTag2);
 
-		UploadPack uploadPack = new UploadPack(remote.getRepository());
+		try (UploadPack uploadPack = new UploadPack(remote.getRepository())) {
 
 		ByteArrayOutputStream cli = new ByteArrayOutputStream();
 		PacketLineOut clientWant = new PacketLineOut(cli);
@@ -2872,7 +2872,6 @@ public void testSingleBranchCloneTagChain() throws Exception {
 		clientWant.writeString("done\n");
 
 		try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) {
-
 			uploadPack.setPreUploadHook(new PreUploadHook() {
 				@Override
 				public void onBeginNegotiateRound(UploadPack up,
@@ -2925,6 +2924,7 @@ public void onSendPack(UploadPack up,
 			assertTrue(objDb.has(heavyTag2.toObjectId()));
 		}
 	}
+}
 
 	@Test
 	public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exception {
@@ -2936,7 +2936,7 @@ public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exceptio
 		RevTag tag3 = remote.tag("t3", tag2);
 		remote.lightweightTag("t3", tag3);
 
-		UploadPack uploadPack = new UploadPack(remote.getRepository());
+		try (UploadPack uploadPack = new UploadPack(remote.getRepository())) {
 
 		ByteArrayOutputStream cli = new ByteArrayOutputStream();
 		PacketLineOut clientWant = new PacketLineOut(cli);
@@ -2946,7 +2946,6 @@ public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exceptio
 		clientWant.writeString("done\n");
 
 		try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) {
-
 			uploadPack.setPreUploadHook(new PreUploadHook() {
 				@Override
 				public void onBeginNegotiateRound(UploadPack up,
@@ -2994,6 +2993,7 @@ public void onSendPack(UploadPack up,
 			assertTrue(objDb.has(one.toObjectId()));
 		}
 	}
+}
 
 	@Test
 	public void testSafeToClearRefsInFetchV0() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java
new file mode 100644
index 0000000..a59d7bc
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012, Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertThrows;
+
+import java.text.ParseException;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests which assert that unparseable Strings lead to ParseExceptions
+ */
+@RunWith(Theories.class)
+public class GitTimeParserBadlyFormattedTest {
+	private String dateStr;
+
+	@Before
+	public void setUp() {
+		MockSystemReader mockSystemReader = new MockSystemReader();
+		SystemReader.setInstance(mockSystemReader);
+	}
+
+	@After
+	public void tearDown() {
+		SystemReader.setInstance(null);
+	}
+
+	public GitTimeParserBadlyFormattedTest(String dateStr) {
+		this.dateStr = dateStr;
+	}
+
+	@DataPoints
+	public static String[] getDataPoints() {
+		return new String[] { "", ".", "...", "1970", "3000.3000.3000", "3 yesterday ago",
+				"now yesterday ago", "yesterdays", "3.day. 2.week.ago",
+				"day ago", "Gra Feb 21 15:35:00 2007 +0100",
+				"Sun Feb 21 15:35:00 2007 +0100",
+				"Wed Feb 21 15:35:00 Grand +0100" };
+	}
+
+	@Theory
+	public void badlyFormattedWithoutRef() {
+		assertThrows(
+				"The expected ParseException while parsing '" + dateStr
+						+ "' did not occur.",
+				ParseException.class, () -> GitTimeParser.parse(dateStr));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java
new file mode 100644
index 0000000..0e5eb28
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024, Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitTimeParserTest {
+	MockSystemReader mockSystemReader;
+
+	@Before
+	public void setUp() {
+		mockSystemReader = new MockSystemReader();
+		SystemReader.setInstance(mockSystemReader);
+	}
+
+	@After
+	public void tearDown() {
+		SystemReader.setInstance(null);
+	}
+
+	@Test
+	public void yesterday() throws ParseException {
+		LocalDateTime parse = GitTimeParser.parse("yesterday");
+
+		LocalDateTime now = SystemReader.getInstance().civilNow();
+		assertEquals(Period.between(parse.toLocalDate(), now.toLocalDate()),
+				Period.ofDays(1));
+	}
+
+	@Test
+	public void never() throws ParseException {
+		LocalDateTime parse = GitTimeParser.parse("never");
+		assertEquals(LocalDateTime.MAX, parse);
+	}
+
+	@Test
+	public void now_pointInTime() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+		LocalDateTime parsedNow = GitTimeParser.parse("now", aTime);
+
+		assertEquals(aTime, parsedNow);
+	}
+
+	@Test
+	public void now_systemTime() throws ParseException {
+		LocalDateTime firstNow = GitTimeParser.parse("now");
+		assertEquals(SystemReader.getInstance().civilNow(), firstNow);
+		mockSystemReader.tick(10);
+		LocalDateTime secondNow = GitTimeParser.parse("now");
+		assertTrue(secondNow.isAfter(firstNow));
+	}
+
+	@Test
+	public void weeksAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+		LocalDateTime parse = GitTimeParser.parse("2 weeks ago", aTime);
+		assertEquals(asLocalDateTime("2007-02-07 15:35:00 +0100"), parse);
+	}
+
+	@Test
+	public void daysAndWeeksAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+		LocalDateTime twoWeeksAgoActual = GitTimeParser.parse("2 weeks ago",
+				aTime);
+
+		LocalDateTime twoWeeksAgoExpected = asLocalDateTime(
+				"2007-02-07 15:35:00 +0100");
+		assertEquals(twoWeeksAgoExpected, twoWeeksAgoActual);
+
+		LocalDateTime combinedWhitespace = GitTimeParser
+				.parse("3 days 2 weeks ago", aTime);
+		LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+				"2007-02-04 15:35:00 +0100");
+		assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+		LocalDateTime combinedDots = GitTimeParser.parse("3.day.2.week.ago",
+				aTime);
+		LocalDateTime combinedDotsExpected = asLocalDateTime(
+				"2007-02-04 15:35:00 +0100");
+		assertEquals(combinedDotsExpected, combinedDots);
+	}
+
+	@Test
+	public void hoursAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:00 +0100");
+
+		LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+				aTime);
+
+		LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+				"2007-02-21 15:35:00 +0100");
+		assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+	}
+
+	@Test
+	public void hoursAgo_acrossDay() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:00 +0100");
+
+		LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+				aTime);
+
+		LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+				"2007-02-20 22:35:00 +0100");
+		assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+	}
+
+	@Test
+	public void minutesHoursAgoCombined() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-04 15:35:00 +0100");
+
+		LocalDateTime combinedWhitespace = GitTimeParser
+				.parse("3 hours 2 minutes ago", aTime);
+		LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+				"2007-02-04 12:33:00 +0100");
+		assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+		LocalDateTime combinedDots = GitTimeParser
+				.parse("3.hours.2.minutes.ago", aTime);
+		LocalDateTime combinedDotsExpected = asLocalDateTime(
+				"2007-02-04 12:33:00 +0100");
+		assertEquals(combinedDotsExpected, combinedDots);
+	}
+
+	@Test
+	public void minutesAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:10 +0100");
+
+		LocalDateTime twoMinutesAgo = GitTimeParser.parse("2 minutes ago",
+				aTime);
+
+		LocalDateTime twoMinutesAgoExpected = asLocalDateTime(
+				"2007-02-21 17:33:10 +0100");
+		assertEquals(twoMinutesAgoExpected, twoMinutesAgo);
+	}
+
+	@Test
+	public void minutesAgo_acrossDay() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:10 +0100");
+
+		LocalDateTime minutesAgoActual = GitTimeParser.parse("40 minutes ago",
+				aTime);
+
+		LocalDateTime minutesAgoExpected = asLocalDateTime(
+				"2007-02-20 23:55:10 +0100");
+		assertEquals(minutesAgoExpected, minutesAgoActual);
+	}
+
+	@Test
+	public void iso() throws ParseException {
+		String dateStr = "2007-02-21 15:35:00 +0100";
+
+		LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+		LocalDateTime expected = asLocalDateTime(dateStr);
+		assertEquals(expected, actual);
+	}
+
+	@Test
+	public void rfc() throws ParseException {
+		String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100";
+
+		LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+		LocalDateTime expected = asLocalDateTime(dateStr,
+				"EEE, dd MMM yyyy HH:mm:ss Z");
+		assertEquals(expected, actual);
+	}
+
+	@Test
+	public void shortFmt() throws ParseException {
+		assertParsing("2007-02-21", "yyyy-MM-dd");
+	}
+
+	@Test
+	public void shortWithDots() throws ParseException {
+		assertParsing("2007.02.21", "yyyy.MM.dd");
+	}
+
+	@Test
+	public void shortWithSlash() throws ParseException {
+		assertParsing("02/21/2007", "MM/dd/yyyy");
+	}
+
+	@Test
+	public void shortWithDotsReverse() throws ParseException {
+		assertParsing("21.02.2007", "dd.MM.yyyy");
+	}
+
+	@Test
+	public void defaultFmt() throws ParseException {
+		assertParsing("Wed Feb 21 15:35:00 2007 +0100",
+				"EEE MMM dd HH:mm:ss yyyy Z");
+	}
+
+	@Test
+	public void local() throws ParseException {
+		assertParsing("Wed Feb 21 15:35:00 2007", "EEE MMM dd HH:mm:ss yyyy");
+	}
+
+	private static void assertParsing(String dateStr, String format)
+			throws ParseException {
+		LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+		LocalDateTime expected = asLocalDateTime(dateStr, format);
+		assertEquals(expected, actual);
+	}
+
+	private static LocalDateTime asLocalDateTime(String dateStr) {
+		return asLocalDateTime(dateStr, "yyyy-MM-dd HH:mm:ss Z");
+	}
+
+	private static LocalDateTime asLocalDateTime(String dateStr,
+			String pattern) {
+		DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
+		TemporalAccessor ta = fmt
+				.withZone(SystemReader.getInstance().getTimeZoneId())
+				.withLocale(SystemReader.getInstance().getLocale())
+				.parse(dateStr);
+		return ta.isSupported(ChronoField.HOUR_OF_DAY) ? LocalDateTime.from(ta)
+				: LocalDate.from(ta).atStartOfDay();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
index 355bbba..e517889 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
@@ -10,10 +10,13 @@
 
 package org.eclipse.jgit.util;
 
+import static java.time.Instant.EPOCH;
+import static java.time.ZoneOffset.UTC;
 import static org.junit.Assert.assertEquals;
 
-import java.util.Date;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Test;
@@ -22,8 +25,8 @@ public class RawParseUtils_ParsePersonIdentTest {
 
 	@Test
 	public void testParsePersonIdent_legalCases() {
-		final Date when = new Date(1234567890000L);
-		final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+		Instant when = Instant.ofEpochMilli(1234567890000L);
+		ZoneId tz = ZoneOffset.ofHours(-7);
 
 		assertPersonIdent("Me <me@example.com> 1234567890 -0700",
 				new PersonIdent("Me", "me@example.com", when, tz));
@@ -50,8 +53,8 @@ public void testParsePersonIdent_legalCases() {
 
 	@Test
 	public void testParsePersonIdent_fuzzyCases() {
-		final Date when = new Date(1234567890000L);
-		final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+		Instant when = Instant.ofEpochMilli(1234567890000L);
+		ZoneId tz = ZoneOffset.ofHours(-7);
 
 		assertPersonIdent(
 				"A U Thor <author@example.com>,  C O. Miter <comiter@example.com> 1234567890 -0700",
@@ -64,8 +67,8 @@ public void testParsePersonIdent_fuzzyCases() {
 
 	@Test
 	public void testParsePersonIdent_incompleteCases() {
-		final Date when = new Date(1234567890000L);
-		final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+		Instant when = Instant.ofEpochMilli(1234567890000L);
+		ZoneId tz = ZoneOffset.ofHours(-7);
 
 		assertPersonIdent("Me <> 1234567890 -0700", new PersonIdent("Me", "",
 				when, tz));
@@ -76,26 +79,26 @@ public void testParsePersonIdent_incompleteCases() {
 		assertPersonIdent(" <> 1234567890 -0700", new PersonIdent("", "", when,
 				tz));
 
-		assertPersonIdent("<>", new PersonIdent("", "", 0, 0));
+		assertPersonIdent("<>", new PersonIdent("", "", EPOCH, UTC));
 
-		assertPersonIdent(" <>", new PersonIdent("", "", 0, 0));
+		assertPersonIdent(" <>", new PersonIdent("", "", EPOCH, UTC));
 
 		assertPersonIdent("<me@example.com>", new PersonIdent("",
-				"me@example.com", 0, 0));
+				"me@example.com", EPOCH, UTC));
 
 		assertPersonIdent(" <me@example.com>", new PersonIdent("",
-				"me@example.com", 0, 0));
+				"me@example.com", EPOCH, UTC));
 
-		assertPersonIdent("Me <>", new PersonIdent("Me", "", 0, 0));
+		assertPersonIdent("Me <>", new PersonIdent("Me", "", EPOCH, UTC));
 
 		assertPersonIdent("Me <me@example.com>", new PersonIdent("Me",
-				"me@example.com", 0, 0));
+				"me@example.com", EPOCH, UTC));
 
 		assertPersonIdent("Me <me@example.com> 1234567890", new PersonIdent(
-				"Me", "me@example.com", 0, 0));
+				"Me", "me@example.com", EPOCH, UTC));
 
 		assertPersonIdent("Me <me@example.com> 1234567890 ", new PersonIdent(
-				"Me", "me@example.com", 0, 0));
+				"Me", "me@example.com", EPOCH, UTC));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
index 7c98d77..399977a 100644
--- a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
@@ -4,14 +4,14 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ui
 Bundle-SymbolicName: org.eclipse.jgit.ui
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.awtui;version="7.0.2"
-Import-Package: org.eclipse.jgit.errors;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revplot;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.2,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.2,7.1.0)"
+Export-Package: org.eclipse.jgit.awtui;version="7.1.2"
+Import-Package: org.eclipse.jgit.errors;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.lib;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.nls;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revplot;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.revwalk;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.transport;version="[7.1.2,7.2.0)",
+ org.eclipse.jgit.util;version="[7.1.2,7.2.0)"
diff --git a/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
index 0d64d01..7ed704c 100644
--- a/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ui - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ui.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ui;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ui;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ui/pom.xml b/org.eclipse.jgit.ui/pom.xml
index ea73b43..90f74a4 100644
--- a/org.eclipse.jgit.ui/pom.xml
+++ b/org.eclipse.jgit.ui/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ui</artifactId>
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
new file mode 100644
index 0000000..023018e
--- /dev/null
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit" version="2">
+    <resource path="src/org/eclipse/jgit/diff/DiffDriver.java" type="org.eclipse.jgit.diff.DiffDriver">
+        <filter id="1109393411">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="org.eclipse.jgit.diff.DiffDriver"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/diff/DiffFormatter.java" type="org.eclipse.jgit.diff.DiffFormatter">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="format(EditList, RawText, RawText, DiffDriver)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="format(FileHeader, RawText, RawText, DiffDriver)"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="writeHunkHeader(int, int, int, int, String)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/Constants.java" type="org.eclipse.jgit.lib.Constants">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="ATTR_BUILTIN_UNION_MERGE_DRIVER"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/merge/ContentMergeStrategy.java" type="org.eclipse.jgit.merge.ContentMergeStrategy">
+        <filter id="1176502275">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="UNION"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger">
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
+                <message_argument value="attributesNodeProvider"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="attributesNodeProvider"/>
+            </message_arguments>
+        </filter>
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="setAttributesNodeProvider(AttributesNodeProvider)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/revwalk/RevWalk.java" type="org.eclipse.jgit.revwalk.RevWalk">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="isMergedIntoAnyCommit(RevCommit, Collection&lt;RevCommit&gt;)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/transport/UploadPack.java" type="org.eclipse.jgit.transport.UploadPack$RequestPolicy">
+        <filter id="1176502275">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="implies(UploadPack.RequestPolicy)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/Iterators.java" type="org.eclipse.jgit.util.Iterators">
+        <filter id="1109393411">
+            <message_arguments>
+                <message_argument value="6.10.2"/>
+                <message_argument value="org.eclipse.jgit.util.Iterators"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
index c4dc76f..ef3d8ec 100644
--- a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
+++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
@@ -40,7 +40,7 @@
 org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=error
 org.eclipse.jdt.core.compiler.problem.invalidJavadoc=error
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
-org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private
 org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 0d4f772..1cce9dc 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -3,14 +3,14 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit
 Bundle-SymbolicName: org.eclipse.jgit
-Bundle-Version: 7.0.2.qualifier
+Bundle-Version: 7.1.2.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Service-Component: OSGI-INF/org.eclipse.jgit.internal.util.CleanupService.xml
 Eclipse-ExtensibleAPI: true
-Export-Package: org.eclipse.jgit.annotations;version="7.0.2",
- org.eclipse.jgit.api;version="7.0.2";
+Export-Package: org.eclipse.jgit.annotations;version="7.1.2",
+ org.eclipse.jgit.api;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.notes,
    org.eclipse.jgit.dircache,
@@ -25,18 +25,18 @@
    org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.blame,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.api.errors;version="7.0.2";
+ org.eclipse.jgit.api.errors;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="7.0.2";
+ org.eclipse.jgit.attributes;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.blame;version="7.0.2";
+ org.eclipse.jgit.blame;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="7.0.2";
+ org.eclipse.jgit.diff;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
@@ -44,53 +44,53 @@
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.dircache;version="7.0.2";
+ org.eclipse.jgit.dircache;version="7.1.2";
   uses:="org.eclipse.jgit.events,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.errors;version="7.0.2";
+ org.eclipse.jgit.errors;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.internal.storage.pack",
- org.eclipse.jgit.events;version="7.0.2";
+ org.eclipse.jgit.events;version="7.1.2";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="7.0.2",
- org.eclipse.jgit.gitrepo;version="7.0.2";
+ org.eclipse.jgit.fnmatch;version="7.1.2",
+ org.eclipse.jgit.gitrepo;version="7.1.2";
   uses:="org.xml.sax.helpers,
    org.eclipse.jgit.api,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.xml.sax",
- org.eclipse.jgit.gitrepo.internal;version="7.0.2";x-internal:=true,
- org.eclipse.jgit.hooks;version="7.0.2";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.ignore;version="7.0.2",
- org.eclipse.jgit.ignore.internal;version="7.0.2";
+ org.eclipse.jgit.gitrepo.internal;version="7.1.2";x-internal:=true,
+ org.eclipse.jgit.hooks;version="7.1.2";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.ignore;version="7.1.2",
+ org.eclipse.jgit.ignore.internal;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="7.0.2";
+ org.eclipse.jgit.internal;version="7.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.diff;version="7.0.2";
+ org.eclipse.jgit.internal.diff;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.diffmergetool;version="7.0.2";
+ org.eclipse.jgit.internal.diffmergetool;version="7.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.pgm,
    org.eclipse.egit.ui",
- org.eclipse.jgit.internal.fsck;version="7.0.2";
+ org.eclipse.jgit.internal.fsck;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.revwalk;version="7.0.2";
+ org.eclipse.jgit.internal.revwalk;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.commitgraph;version="7.0.2";
+ org.eclipse.jgit.internal.storage.commitgraph;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.dfs;version="7.0.2";
+ org.eclipse.jgit.internal.storage.dfs;version="7.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.server,
    org.eclipse.jgit.http.test,
    org.eclipse.jgit.lfs.test",
- org.eclipse.jgit.internal.storage.file;version="7.0.2";
+ org.eclipse.jgit.internal.storage.file;version="7.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
@@ -102,36 +102,36 @@
    org.eclipse.jgit.ssh.apache,
    org.eclipse.jgit.ssh.apache.test,
    org.eclipse.jgit.ssh.jsch.test",
- org.eclipse.jgit.internal.storage.io;version="7.0.2";
+ org.eclipse.jgit.internal.storage.io;version="7.1.2";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.memory;version="7.0.2";
+ org.eclipse.jgit.internal.storage.memory;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.pack;version="7.0.2";
+ org.eclipse.jgit.internal.storage.pack;version="7.1.2";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftable;version="7.0.2";
+ org.eclipse.jgit.internal.storage.reftable;version="7.1.2";
   x-friends:="org.eclipse.jgit.http.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.submodule;version="7.0.2";x-internal:=true,
- org.eclipse.jgit.internal.transport.connectivity;version="7.0.2";
+ org.eclipse.jgit.internal.submodule;version="7.1.2";x-internal:=true,
+ org.eclipse.jgit.internal.transport.connectivity;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.http;version="7.0.2";
+ org.eclipse.jgit.internal.transport.http;version="7.1.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.parser;version="7.0.2";
+ org.eclipse.jgit.internal.transport.parser;version="7.1.2";
   x-friends:="org.eclipse.jgit.http.server,
    org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.ssh;version="7.0.2";
+ org.eclipse.jgit.internal.transport.ssh;version="7.1.2";
   x-friends:="org.eclipse.jgit.ssh.apache,
    org.eclipse.jgit.ssh.jsch,
    org.eclipse.jgit.test",
- org.eclipse.jgit.internal.util;version="7.0.2";
+ org.eclipse.jgit.internal.util;version="7.1.2";
   x-friends:=" org.eclipse.jgit.junit",
- org.eclipse.jgit.lib;version="7.0.2";
+ org.eclipse.jgit.lib;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.sha1,
    org.eclipse.jgit.dircache,
@@ -145,12 +145,12 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.submodule,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.lib.internal;version="7.0.2";
+ org.eclipse.jgit.lib.internal;version="7.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.pgm,
    org.eclipse.egit.ui",
- org.eclipse.jgit.logging;version="7.0.2",
- org.eclipse.jgit.merge;version="7.0.2";
+ org.eclipse.jgit.logging;version="7.1.2",
+ org.eclipse.jgit.merge;version="7.1.2";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -159,40 +159,40 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.api,
    org.eclipse.jgit.attributes",
- org.eclipse.jgit.nls;version="7.0.2",
- org.eclipse.jgit.notes;version="7.0.2";
+ org.eclipse.jgit.nls;version="7.1.2",
+ org.eclipse.jgit.notes;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="7.0.2";
+ org.eclipse.jgit.patch;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="7.0.2";
+ org.eclipse.jgit.revplot;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="7.0.2";
+ org.eclipse.jgit.revwalk;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.revwalk.filter;version="7.0.2";
+ org.eclipse.jgit.revwalk.filter;version="7.1.2";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="7.0.2";
+ org.eclipse.jgit.storage.file;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="7.0.2";
+ org.eclipse.jgit.storage.pack;version="7.1.2";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="7.0.2";
+ org.eclipse.jgit.submodule;version="7.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.transport;version="7.0.2";
+ org.eclipse.jgit.transport;version="7.1.2";
   uses:="javax.crypto,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.lib,
@@ -205,21 +205,21 @@
    org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.storage.pack,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.transport.http;version="7.0.2";
+ org.eclipse.jgit.transport.http;version="7.1.2";
   uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="7.0.2";
+ org.eclipse.jgit.transport.resolver;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.lib",
- org.eclipse.jgit.treewalk;version="7.0.2";
+ org.eclipse.jgit.treewalk;version="7.1.2";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util",
- org.eclipse.jgit.treewalk.filter;version="7.0.2";
+ org.eclipse.jgit.treewalk.filter;version="7.1.2";
   uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="7.0.2";
+ org.eclipse.jgit.util;version="7.1.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.hooks,
    org.eclipse.jgit.revwalk,
@@ -232,12 +232,12 @@
    org.eclipse.jgit.treewalk,
    javax.net.ssl,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.util.io;version="7.0.2";
+ org.eclipse.jgit.util.io;version="7.1.2";
   uses:="org.eclipse.jgit.attributes,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util.sha1;version="7.0.2",
- org.eclipse.jgit.util.time;version="7.0.2"
+ org.eclipse.jgit.util.sha1;version="7.1.2",
+ org.eclipse.jgit.util.time;version="7.1.2"
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  javax.crypto,
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index 2315007..1bf5168 100644
--- a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit - Sources
 Bundle-SymbolicName: org.eclipse.jgit.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit;version="7.0.2.qualifier";roots="."
+Bundle-Version: 7.1.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit;version="7.1.2.qualifier";roots="."
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index a18b1fd..df45481 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.2-SNAPSHOT</version>
+    <version>7.1.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit</artifactId>
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index fd0995a..4e2073b 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -500,7 +500,7 @@
 mergeStrategyAlreadyExistsAsDefault=Merge strategy "{0}" already exists as a default strategy
 mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads to be merged into HEAD
 mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4}
-mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}
+mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}\nFailing paths: {2}
 mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n  count {3}"
 mergeToolNotGivenError=No merge tool provided and no defaults configured.
 mergeToolNullError=Parameter for merge tool cannot be null.
@@ -595,6 +595,8 @@
 packingCancelledDuringObjectsWriting=Packing cancelled during objects writing
 packObjectCountMismatch=Pack object count mismatch: pack {0} index {1}: {2}
 packRefs=Pack refs
+packRefsFailed=Packing refs failed
+packRefsSuccessful=Packed refs successfully
 packSizeNotSetYet=Pack size not yet set since it has not yet been received
 packTooLargeForIndexVersion1=Pack too large for index version 1
 packWasDeleted=Pack file {0} was deleted, removing it from pack list
@@ -638,8 +640,6 @@
 readerIsRequired=Reader is required
 readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0}
 readLastModifiedFailed=Reading lastModified of {0} failed
-readPipeIsNotAllowed=FS.readPipe() isn't allowed for command ''{0}''. Working directory: ''{1}''.
-readPipeIsNotAllowedRequiredPermission=FS.readPipe() isn't allowed for command ''{0}''. Working directory: ''{1}''. Required permission: {2}.
 readTimedOut=Read timed out after {0} ms
 receivePackObjectTooLarge1=Object too large, rejecting the pack. Max object size limit is {0} bytes.
 receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max object size limit is {1} bytes.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
index 3dc53ec..5bc035a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -714,6 +714,16 @@ public GarbageCollectCommand gc() {
 	}
 
 	/**
+	 * Return a command object to execute a {@code PackRefs} command
+	 *
+	 * @return a {@link org.eclipse.jgit.api.PackRefsCommand}
+	 * @since 7.1
+	 */
+	public PackRefsCommand packRefs() {
+		return new PackRefsCommand(repo);
+	}
+
+	/**
 	 * Return a command object to find human-readable names of revisions.
 	 *
 	 * @return a {@link org.eclipse.jgit.api.NameRevCommand}.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java
new file mode 100644
index 0000000..29a69c5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.api;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Optimize storage of references.
+ *
+ * @since 7.1
+ */
+public class PackRefsCommand extends GitCommand<String> {
+	private ProgressMonitor monitor;
+
+	private boolean all;
+
+	/**
+	 * Creates a new {@link PackRefsCommand} instance with default values.
+	 *
+	 * @param repo
+	 * 		the repository this command will be used on
+	 */
+	public PackRefsCommand(Repository repo) {
+		super(repo);
+		this.monitor = NullProgressMonitor.INSTANCE;
+	}
+
+	/**
+	 * Set progress monitor
+	 *
+	 * @param monitor
+	 * 		a progress monitor
+	 * @return this instance
+	 */
+	public PackRefsCommand setProgressMonitor(ProgressMonitor monitor) {
+		this.monitor = monitor;
+		return this;
+	}
+
+	/**
+	 * Specify whether to pack all the references.
+	 *
+	 * @param all
+	 * 		if <code>true</code> all the loose refs will be packed
+	 * @return this instance
+	 */
+	public PackRefsCommand setAll(boolean all) {
+		this.all = all;
+		return this;
+	}
+
+	/**
+	 * Whether to pack all the references
+	 *
+	 * @return whether to pack all the references
+	 */
+	public boolean isAll() {
+		return all;
+	}
+
+	@Override
+	public String call() throws GitAPIException {
+		checkCallable();
+		try {
+			repo.getRefDatabase().packRefs(monitor, this);
+			return JGitText.get().packRefsSuccessful;
+		} catch (IOException e) {
+			throw new JGitInternalException(JGitText.get().packRefsFailed, e);
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
index 76dc87e..fdfe533 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
@@ -360,18 +360,22 @@ public static boolean isBinary(byte[] raw, int length, boolean complete) {
 			length = maxLength;
 			isComplete = false;
 		}
-		byte last = 'x'; // Just something inconspicuous.
-		for (int ptr = 0; ptr < length; ptr++) {
-			byte curr = raw[ptr];
-			if (isBinary(curr, last)) {
+
+		int ptr = -1;
+		byte current;
+		while (ptr < length - 2) {
+			current = raw[++ptr];
+			if (current == '\0' || (current == '\r' && raw[++ptr] != '\n')) {
 				return true;
 			}
-			last = curr;
 		}
-		if (isComplete) {
-			// Buffer contains everything...
-			return last == '\r'; // ... so this must be a lone CR
+
+		if (ptr == length - 2) {
+			// if '\r' be last, then if isComplete then return binary
+			current = raw[++ptr];
+			return current == '\0' || (current == '\r' && isComplete);
 		}
+
 		return false;
 	}
 
@@ -467,26 +471,30 @@ public static boolean isCrLfText(byte[] raw, int length) {
 	 */
 	public static boolean isCrLfText(byte[] raw, int length, boolean complete) {
 		boolean has_crlf = false;
-		byte last = 'x'; // Just something inconspicuous
-		for (int ptr = 0; ptr < length; ptr++) {
-			byte curr = raw[ptr];
-			if (isBinary(curr, last)) {
+
+		int ptr = -1;
+		byte current;
+		while (ptr < length - 2) {
+			current = raw[++ptr];
+			if (current == '\0') {
 				return false;
 			}
-			if (curr == '\n' && last == '\r') {
+			if (current == '\r') {
+				if (raw[++ptr] != '\n') {
+					return false;
+				}
 				has_crlf = true;
 			}
-			last = curr;
 		}
-		if (last == '\r') {
-			if (complete) {
-				// Lone CR: it's binary after all.
+
+		if (ptr == length - 2) {
+			// if '\r' be last, then if isComplete then return binary
+			current = raw[++ptr];
+			if (current == '\0' || (current == '\r' && complete)) {
 				return false;
 			}
-			// Tough call. If the next byte, which we don't have, would be a
-			// '\n', it'd be a CR-LF text, otherwise it'd be binary. Just decide
-			// based on what we already scanned; it wasn't binary until now.
 		}
+
 		return has_crlf;
 	}
 
@@ -578,4 +586,5 @@ public static RawText load(ObjectLoader ldr, int threshold)
 			return new RawText(data, RawParseUtils.lineMapOrBinary(data, 0, (int) sz));
 		}
 	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
index 5de7bac..fb98df7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
@@ -80,7 +80,7 @@ class SimilarityRenameDetector {
 	private long[] matrix;
 
 	/** Score a pair must exceed to be considered a rename. */
-	private int renameScore = 60;
+	private int renameScore = 50;
 
 	/**
 	 * File size threshold (in bytes) for detecting renames. Files larger
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index c309182..c8012d6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -625,6 +625,8 @@ public static JGitText get() {
 	/***/ public String packingCancelledDuringObjectsWriting;
 	/***/ public String packObjectCountMismatch;
 	/***/ public String packRefs;
+	/***/ public String packRefsFailed;
+	/***/ public String packRefsSuccessful;
 	/***/ public String packSizeNotSetYet;
 	/***/ public String packTooLargeForIndexVersion1;
 	/***/ public String packWasDeleted;
@@ -668,8 +670,6 @@ public static JGitText get() {
 	/***/ public String readerIsRequired;
 	/***/ public String readingObjectsFromLocalRepositoryFailed;
 	/***/ public String readLastModifiedFailed;
-	/***/ public String readPipeIsNotAllowed;
-	/***/ public String readPipeIsNotAllowedRequiredPermission;
 	/***/ public String readTimedOut;
 	/***/ public String receivePackObjectTooLarge1;
 	/***/ public String receivePackObjectTooLarge2;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java
new file mode 100644
index 0000000..295b702
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+
+/**
+ * Aggregates values for all given {@link BlockCacheStats}.
+ */
+class AggregatedBlockCacheStats implements BlockCacheStats {
+	private final List<BlockCacheStats> blockCacheStats;
+
+	static BlockCacheStats fromStatsList(
+			List<BlockCacheStats> blockCacheStats) {
+		if (blockCacheStats.size() == 1) {
+			return blockCacheStats.get(0);
+		}
+		return new AggregatedBlockCacheStats(blockCacheStats);
+	}
+
+	private AggregatedBlockCacheStats(List<BlockCacheStats> blockCacheStats) {
+		this.blockCacheStats = blockCacheStats;
+	}
+
+	@Override
+	public String getName() {
+		return AggregatedBlockCacheStats.class.getName();
+	}
+
+	@Override
+	public long[] getCurrentSize() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getCurrentSize());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getHitCount() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getHitCount());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getMissCount() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getMissCount());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getTotalRequestCount() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getTotalRequestCount());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getHitRatio() {
+		long[] hit = getHitCount();
+		long[] miss = getMissCount();
+		long[] ratio = new long[Math.max(hit.length, miss.length)];
+		for (int i = 0; i < ratio.length; i++) {
+			if (i >= hit.length) {
+				ratio[i] = 0;
+			} else if (i >= miss.length) {
+				ratio[i] = 100;
+			} else {
+				long total = hit[i] + miss[i];
+				ratio[i] = total == 0 ? 0 : hit[i] * 100 / total;
+			}
+		}
+		return ratio;
+	}
+
+	@Override
+	public long[] getEvictions() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getEvictions());
+		}
+		return sums;
+	}
+
+	private static long[] emptyPackStats() {
+		return new long[PackExt.values().length];
+	}
+
+	private static long[] add(long[] first, long[] second) {
+		long[] sums = new long[Integer.max(first.length, second.length)];
+		int i;
+		for (i = 0; i < Integer.min(first.length, second.length); i++) {
+			sums[i] = first[i] + second[i];
+		}
+		for (int j = i; j < first.length; j++) {
+			sums[j] = first[i];
+		}
+		for (int j = i; j < second.length; j++) {
+			sums[j] = second[i];
+		}
+		return sums;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java
index ce71a71..587d482 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java
@@ -12,6 +12,7 @@
 
 import java.io.IOException;
 import java.time.Duration;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -47,6 +48,11 @@
  * invocations is also fixed in size.
  */
 final class ClockBlockCacheTable implements DfsBlockCacheTable {
+	/**
+	 * Table name.
+	 */
+	private final String name;
+
 	/** Number of entries in {@link #table}. */
 	private final int tableSize;
 
@@ -129,14 +135,20 @@ final class ClockBlockCacheTable implements DfsBlockCacheTable {
 				-1, 0, null);
 		clockHand.next = clockHand;
 
-		this.dfsBlockCacheStats = new DfsBlockCacheStats();
+		this.name = cfg.getName();
+		this.dfsBlockCacheStats = new DfsBlockCacheStats(this.name);
 		this.refLockWaitTime = cfg.getRefLockWaitTimeConsumer();
 		this.indexEventConsumer = cfg.getIndexEventConsumer();
 	}
 
 	@Override
-	public BlockCacheStats getBlockCacheStats() {
-		return dfsBlockCacheStats;
+	public List<BlockCacheStats> getBlockCacheStats() {
+		return List.of(dfsBlockCacheStats);
+	}
+
+	@Override
+	public String getName() {
+		return name;
 	}
 
 	@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
index 3e1300c..f8e0831 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
@@ -11,7 +11,10 @@
 
 package org.eclipse.jgit.internal.storage.dfs;
 
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+
 import java.io.IOException;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.LongStream;
 
@@ -97,7 +100,12 @@ private DfsBlockCache(DfsBlockCacheConfig cfg) {
 		double streamRatio = cfg.getStreamRatio();
 		maxStreamThroughCache = (long) (maxBytes * streamRatio);
 
-		dfsBlockCacheTable = new ClockBlockCacheTable(cfg);
+		if (!cfg.getPackExtCacheConfigurations().isEmpty()) {
+			dfsBlockCacheTable = PackExtBlockCacheTable
+					.fromBlockCacheConfigs(cfg);
+		} else {
+			dfsBlockCacheTable = new ClockBlockCacheTable(cfg);
+		}
 
 		for (int i = 0; i < PackExt.values().length; ++i) {
 			Integer limit = cfg.getCacheHotMap().get(PackExt.values()[i]);
@@ -119,7 +127,7 @@ boolean shouldCopyThroughCache(long length) {
 	 * @return total number of bytes in the cache, per pack file extension.
 	 */
 	public long[] getCurrentSize() {
-		return dfsBlockCacheTable.getBlockCacheStats().getCurrentSize();
+		return getAggregatedBlockCacheStats().getCurrentSize();
 	}
 
 	/**
@@ -138,7 +146,7 @@ public long getFillPercentage() {
 	 *         extension.
 	 */
 	public long[] getHitCount() {
-		return dfsBlockCacheTable.getBlockCacheStats().getHitCount();
+		return getAggregatedBlockCacheStats().getHitCount();
 	}
 
 	/**
@@ -149,7 +157,7 @@ public long getFillPercentage() {
 	 *         extension.
 	 */
 	public long[] getMissCount() {
-		return dfsBlockCacheTable.getBlockCacheStats().getMissCount();
+		return getAggregatedBlockCacheStats().getMissCount();
 	}
 
 	/**
@@ -158,8 +166,7 @@ public long getFillPercentage() {
 	 * @return total number of requests (hit + miss), per pack file extension.
 	 */
 	public long[] getTotalRequestCount() {
-		return dfsBlockCacheTable.getBlockCacheStats()
-				.getTotalRequestCount();
+		return getAggregatedBlockCacheStats().getTotalRequestCount();
 	}
 
 	/**
@@ -168,7 +175,7 @@ public long getFillPercentage() {
 	 * @return hit ratios
 	 */
 	public long[] getHitRatio() {
-		return dfsBlockCacheTable.getBlockCacheStats().getHitRatio();
+		return getAggregatedBlockCacheStats().getHitRatio();
 	}
 
 	/**
@@ -179,7 +186,18 @@ public long getFillPercentage() {
 	 *         file extension.
 	 */
 	public long[] getEvictions() {
-		return dfsBlockCacheTable.getBlockCacheStats().getEvictions();
+		return getAggregatedBlockCacheStats().getEvictions();
+	}
+
+	/**
+	 * Get the list of {@link BlockCacheStats} for all underlying caches.
+	 * <p>
+	 * Useful in monitoring caches with breakdown.
+	 *
+	 * @return the list of {@link BlockCacheStats} for all underlying caches.
+	 */
+	public List<BlockCacheStats> getAllBlockCacheStats() {
+		return dfsBlockCacheTable.getBlockCacheStats();
 	}
 
 	/**
@@ -259,6 +277,11 @@ <T> T get(DfsStreamKey key, long position) {
 		return dfsBlockCacheTable.get(key, position);
 	}
 
+	private BlockCacheStats getAggregatedBlockCacheStats() {
+		return AggregatedBlockCacheStats
+				.fromStatsList(dfsBlockCacheTable.getBlockCacheStats());
+	}
+
 	static final class Ref<T> {
 		final DfsStreamKey key;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
index fa68b97..17bf518 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
@@ -19,6 +19,7 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_EXTENSIONS;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO;
 
+import java.io.PrintWriter;
 import java.text.MessageFormat;
 import java.time.Duration;
 import java.util.ArrayList;
@@ -29,6 +30,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import org.eclipse.jgit.internal.JGitText;
@@ -49,6 +51,10 @@ public class DfsBlockCacheConfig {
 	/** Default number of max cache hits. */
 	public static final int DEFAULT_CACHE_HOT_MAX = 1;
 
+	static final String DEFAULT_NAME = "<default>"; //$NON-NLS-1$
+
+	private String name;
+
 	private long blockLimit;
 
 	private int blockSize;
@@ -69,6 +75,7 @@ public class DfsBlockCacheConfig {
 	 * Create a default configuration.
 	 */
 	public DfsBlockCacheConfig() {
+		name = DEFAULT_NAME;
 		setBlockLimit(32 * MB);
 		setBlockSize(64 * KB);
 		setStreamRatio(0.30);
@@ -78,6 +85,72 @@ public DfsBlockCacheConfig() {
 	}
 
 	/**
+	 * Print the current cache configuration to the given {@link PrintWriter}.
+	 *
+	 * @param writer
+	 *            {@link PrintWriter} to write the cache's configuration to.
+	 */
+	public void print(PrintWriter writer) {
+		print(/* linePrefix= */ "", /* pad= */ "  ", writer); //$NON-NLS-1$//$NON-NLS-2$
+	}
+
+	/**
+	 * Print the current cache configuration to the given {@link PrintWriter}.
+	 *
+	 * @param linePrefix
+	 *            prefix to prepend all writen lines with. Ex a string of 0 or
+	 *            more " " entries.
+	 * @param pad
+	 *            filler used to extend linePrefix. Ex a multiple of " ".
+	 * @param writer
+	 *            {@link PrintWriter} to write the cache's configuration to.
+	 */
+	@SuppressWarnings("nls")
+	private void print(String linePrefix, String pad, PrintWriter writer) {
+		String currentPrefixLevel = linePrefix;
+		if (!name.isEmpty() || !packExtCacheConfigurations.isEmpty()) {
+			writer.println(linePrefix + "Name: "
+					+ (name.isEmpty() ? DEFAULT_NAME : this.name));
+			currentPrefixLevel += pad;
+		}
+		writer.println(currentPrefixLevel + "BlockLimit: " + blockLimit);
+		writer.println(currentPrefixLevel + "BlockSize: " + blockSize);
+		writer.println(currentPrefixLevel + "StreamRatio: " + streamRatio);
+		writer.println(
+				currentPrefixLevel + "ConcurrencyLevel: " + concurrencyLevel);
+		for (Map.Entry<PackExt, Integer> entry : cacheHotMap.entrySet()) {
+			writer.println(currentPrefixLevel + "CacheHotMapEntry: "
+					+ entry.getKey() + " : " + entry.getValue());
+		}
+		for (DfsBlockCachePackExtConfig extConfig : packExtCacheConfigurations) {
+			extConfig.print(currentPrefixLevel, pad, writer);
+		}
+	}
+
+	/**
+	 * Get the name for the block cache configured by this cache config.
+	 *
+	 * @return the name for the block cache configured by this cache config.
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Set the name for the block cache configured by this cache config.
+	 * <p>
+	 * Made visible for testing.
+	 *
+	 * @param name
+	 *            the name for the block cache configured by this cache config.
+	 * @return {@code this}
+	 */
+	DfsBlockCacheConfig setName(String name) {
+		this.name = name;
+		return this;
+	}
+
+	/**
 	 * Get maximum number bytes of heap memory to dedicate to caching pack file
 	 * data.
 	 *
@@ -226,12 +299,24 @@ public Map<PackExt, Integer> getCacheHotMap() {
 	 *            map of hot count per pack extension for {@code DfsBlockCache}.
 	 * @return {@code this}
 	 */
+	/*
+	 * TODO The cache HotMap configuration should be set as a config option and
+	 * not passed in through a setter.
+	 */
 	public DfsBlockCacheConfig setCacheHotMap(
 			Map<PackExt, Integer> cacheHotMap) {
 		this.cacheHotMap = Collections.unmodifiableMap(cacheHotMap);
+		setCacheHotMapToPackExtConfigs(this.cacheHotMap);
 		return this;
 	}
 
+	private void setCacheHotMapToPackExtConfigs(
+			Map<PackExt, Integer> cacheHotMap) {
+		for (DfsBlockCachePackExtConfig packExtConfig : packExtCacheConfigurations) {
+			packExtConfig.setCacheHotMap(cacheHotMap);
+		}
+	}
+
 	/**
 	 * Get the consumer of cache index events.
 	 *
@@ -308,6 +393,11 @@ private void fromConfig(String section, String subSection, Config rc) {
 					Long.valueOf(cfgBlockLimit), Long.valueOf(cfgBlockSize)));
 		}
 
+		// Set name only if `core dfs` is configured, otherwise fall back to the
+		// default.
+		if (rc.getSubsections(section).contains(subSection)) {
+			this.name = subSection;
+		}
 		setBlockLimit(cfgBlockLimit);
 		setBlockSize(cfgBlockSize);
 
@@ -354,6 +444,7 @@ private void loadPackExtConfigs(Config config) {
 			cacheConfigs.add(cacheConfig);
 		}
 		packExtCacheConfigurations = cacheConfigs;
+		setCacheHotMapToPackExtConfigs(this.cacheHotMap);
 	}
 
 	private static <T> Set<T> intersection(Set<T> first, Set<T> second) {
@@ -472,6 +563,14 @@ DfsBlockCacheConfig getPackExtCacheConfiguration() {
 			return packExtCacheConfiguration;
 		}
 
+		void setCacheHotMap(Map<PackExt, Integer> cacheHotMap) {
+			Map<PackExt, Integer> packExtHotMap = packExts.stream()
+					.filter(cacheHotMap::containsKey)
+					.collect(Collectors.toUnmodifiableMap(Function.identity(),
+							cacheHotMap::get));
+			packExtCacheConfiguration.setCacheHotMap(packExtHotMap);
+		}
+
 		private static DfsBlockCachePackExtConfig fromConfig(Config config,
 				String section, String subSection) {
 			String packExtensions = config.getString(section, subSection,
@@ -498,5 +597,11 @@ private static DfsBlockCachePackExtConfig fromConfig(Config config,
 			return new DfsBlockCachePackExtConfig(EnumSet.copyOf(packExts),
 					dfsBlockCacheConfig);
 		}
+
+		void print(String linePrefix, String pad, PrintWriter writer) {
+			packExtCacheConfiguration.print(linePrefix, pad, writer);
+			writer.println(linePrefix + pad + "PackExts: " //$NON-NLS-1$
+					+ packExts.stream().sorted().collect(Collectors.toList()));
+		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java
new file mode 100644
index 0000000..436f574
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+
+/**
+ * Keeps track of stats for a Block Cache table.
+ */
+class DfsBlockCacheStats implements BlockCacheStats {
+	private final String name;
+
+	/**
+	 * Number of times a block was found in the cache, per pack file extension.
+	 */
+	private final AtomicReference<AtomicLong[]> statHit;
+
+	/**
+	 * Number of times a block was not found, and had to be loaded, per pack
+	 * file extension.
+	 */
+	private final AtomicReference<AtomicLong[]> statMiss;
+
+	/**
+	 * Number of blocks evicted due to cache being full, per pack file
+	 * extension.
+	 */
+	private final AtomicReference<AtomicLong[]> statEvict;
+
+	/**
+	 * Number of bytes currently loaded in the cache, per pack file extension.
+	 */
+	private final AtomicReference<AtomicLong[]> liveBytes;
+
+	DfsBlockCacheStats() {
+		this(""); //$NON-NLS-1$
+	}
+
+	DfsBlockCacheStats(String name) {
+		this.name = name;
+		statHit = new AtomicReference<>(newCounters());
+		statMiss = new AtomicReference<>(newCounters());
+		statEvict = new AtomicReference<>(newCounters());
+		liveBytes = new AtomicReference<>(newCounters());
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Increment the {@code statHit} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 */
+	void incrementHit(DfsStreamKey key) {
+		getStat(statHit, key).incrementAndGet();
+	}
+
+	/**
+	 * Increment the {@code statMiss} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 */
+	void incrementMiss(DfsStreamKey key) {
+		getStat(statMiss, key).incrementAndGet();
+	}
+
+	/**
+	 * Increment the {@code statEvict} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 */
+	void incrementEvict(DfsStreamKey key) {
+		getStat(statEvict, key).incrementAndGet();
+	}
+
+	/**
+	 * Add {@code size} to the {@code liveBytes} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 * @param size
+	 *            amount to increment the count by.
+	 */
+	void addToLiveBytes(DfsStreamKey key, long size) {
+		getStat(liveBytes, key).addAndGet(size);
+	}
+
+	@Override
+	public long[] getCurrentSize() {
+		return getStatVals(liveBytes);
+	}
+
+	@Override
+	public long[] getHitCount() {
+		return getStatVals(statHit);
+	}
+
+	@Override
+	public long[] getMissCount() {
+		return getStatVals(statMiss);
+	}
+
+	@Override
+	public long[] getTotalRequestCount() {
+		AtomicLong[] hit = statHit.get();
+		AtomicLong[] miss = statMiss.get();
+		long[] cnt = new long[Math.max(hit.length, miss.length)];
+		for (int i = 0; i < hit.length; i++) {
+			cnt[i] += hit[i].get();
+		}
+		for (int i = 0; i < miss.length; i++) {
+			cnt[i] += miss[i].get();
+		}
+		return cnt;
+	}
+
+	@Override
+	public long[] getHitRatio() {
+		AtomicLong[] hit = statHit.get();
+		AtomicLong[] miss = statMiss.get();
+		long[] ratio = new long[Math.max(hit.length, miss.length)];
+		for (int i = 0; i < ratio.length; i++) {
+			if (i >= hit.length) {
+				ratio[i] = 0;
+			} else if (i >= miss.length) {
+				ratio[i] = 100;
+			} else {
+				long hitVal = hit[i].get();
+				long missVal = miss[i].get();
+				long total = hitVal + missVal;
+				ratio[i] = total == 0 ? 0 : hitVal * 100 / total;
+			}
+		}
+		return ratio;
+	}
+
+	@Override
+	public long[] getEvictions() {
+		return getStatVals(statEvict);
+	}
+
+	private static AtomicLong[] newCounters() {
+		AtomicLong[] ret = new AtomicLong[PackExt.values().length];
+		for (int i = 0; i < ret.length; i++) {
+			ret[i] = new AtomicLong();
+		}
+		return ret;
+	}
+
+	private static long[] getStatVals(AtomicReference<AtomicLong[]> stat) {
+		AtomicLong[] stats = stat.get();
+		long[] cnt = new long[stats.length];
+		for (int i = 0; i < stats.length; i++) {
+			cnt[i] = stats[i].get();
+		}
+		return cnt;
+	}
+
+	private static AtomicLong getStat(AtomicReference<AtomicLong[]> stats,
+			DfsStreamKey key) {
+		int pos = key.packExtPos;
+		while (true) {
+			AtomicLong[] vals = stats.get();
+			if (pos < vals.length) {
+				return vals[pos];
+			}
+			AtomicLong[] expect = vals;
+			vals = new AtomicLong[Math.max(pos + 1, PackExt.values().length)];
+			System.arraycopy(expect, 0, vals, 0, expect.length);
+			for (int i = expect.length; i < vals.length; i++) {
+				vals[i] = new AtomicLong();
+			}
+			if (stats.compareAndSet(expect, vals)) {
+				return vals[pos];
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java
index 309f2d1..c3fd07b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java
@@ -11,10 +11,7 @@
 package org.eclipse.jgit.internal.storage.dfs;
 
 import java.io.IOException;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.eclipse.jgit.internal.storage.pack.PackExt;
+import java.util.List;
 
 /**
  * Block cache table.
@@ -129,18 +126,37 @@ <T> DfsBlockCache.Ref<T> getOrLoadRef(DfsStreamKey key, long position,
 	<T> T get(DfsStreamKey key, long position);
 
 	/**
-	 * Get the {@link BlockCacheStats} object for this block cache table's
-	 * statistics.
+	 * Get the list of {@link BlockCacheStats} held by this cache.
+	 * <p>
+	 * The returned list has a {@link BlockCacheStats} per configured cache
+	 * table, with a minimum of 1 {@link BlockCacheStats} object returned.
 	 *
-	 * @return the {@link BlockCacheStats} tracking this block cache table's
-	 *         statistics.
+	 * Use {@link AggregatedBlockCacheStats} to combine the results of the stats
+	 * in the list for an aggregated view of the cache's stats.
+	 *
+	 * @return the list of {@link BlockCacheStats} held by this cache.
 	 */
-	BlockCacheStats getBlockCacheStats();
+	List<BlockCacheStats> getBlockCacheStats();
+
+	/**
+	 * Get the name of the table.
+	 *
+	 * @return this table's name.
+	 */
+	String getName();
 
 	/**
 	 * Provides methods used with Block Cache statistics.
 	 */
 	interface BlockCacheStats {
+
+		/**
+		 * Get the name of the block cache generating this instance.
+		 *
+		 * @return this cache's name.
+		 */
+		String getName();
+
 		/**
 		 * Get total number of bytes in the cache, per pack file extension.
 		 *
@@ -190,174 +206,4 @@ interface BlockCacheStats {
 		 */
 		long[] getEvictions();
 	}
-
-	/**
-	 * Keeps track of stats for a Block Cache table.
-	 */
-	class DfsBlockCacheStats implements BlockCacheStats {
-		/**
-		 * Number of times a block was found in the cache, per pack file
-		 * extension.
-		 */
-		private final AtomicReference<AtomicLong[]> statHit;
-
-		/**
-		 * Number of times a block was not found, and had to be loaded, per pack
-		 * file extension.
-		 */
-		private final AtomicReference<AtomicLong[]> statMiss;
-
-		/**
-		 * Number of blocks evicted due to cache being full, per pack file
-		 * extension.
-		 */
-		private final AtomicReference<AtomicLong[]> statEvict;
-
-		/**
-		 * Number of bytes currently loaded in the cache, per pack file
-		 * extension.
-		 */
-		private final AtomicReference<AtomicLong[]> liveBytes;
-
-		DfsBlockCacheStats() {
-			statHit = new AtomicReference<>(newCounters());
-			statMiss = new AtomicReference<>(newCounters());
-			statEvict = new AtomicReference<>(newCounters());
-			liveBytes = new AtomicReference<>(newCounters());
-		}
-
-		/**
-		 * Increment the {@code statHit} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 */
-		void incrementHit(DfsStreamKey key) {
-			getStat(statHit, key).incrementAndGet();
-		}
-
-		/**
-		 * Increment the {@code statMiss} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 */
-		void incrementMiss(DfsStreamKey key) {
-			getStat(statMiss, key).incrementAndGet();
-		}
-
-		/**
-		 * Increment the {@code statEvict} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 */
-		void incrementEvict(DfsStreamKey key) {
-			getStat(statEvict, key).incrementAndGet();
-		}
-
-		/**
-		 * Add {@code size} to the {@code liveBytes} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 * @param size
-		 *            amount to increment the count by.
-		 */
-		void addToLiveBytes(DfsStreamKey key, long size) {
-			getStat(liveBytes, key).addAndGet(size);
-		}
-
-		@Override
-		public long[] getCurrentSize() {
-			return getStatVals(liveBytes);
-		}
-
-		@Override
-		public long[] getHitCount() {
-			return getStatVals(statHit);
-		}
-
-		@Override
-		public long[] getMissCount() {
-			return getStatVals(statMiss);
-		}
-
-		@Override
-		public long[] getTotalRequestCount() {
-			AtomicLong[] hit = statHit.get();
-			AtomicLong[] miss = statMiss.get();
-			long[] cnt = new long[Math.max(hit.length, miss.length)];
-			for (int i = 0; i < hit.length; i++) {
-				cnt[i] += hit[i].get();
-			}
-			for (int i = 0; i < miss.length; i++) {
-				cnt[i] += miss[i].get();
-			}
-			return cnt;
-		}
-
-		@Override
-		public long[] getHitRatio() {
-			AtomicLong[] hit = statHit.get();
-			AtomicLong[] miss = statMiss.get();
-			long[] ratio = new long[Math.max(hit.length, miss.length)];
-			for (int i = 0; i < ratio.length; i++) {
-				if (i >= hit.length) {
-					ratio[i] = 0;
-				} else if (i >= miss.length) {
-					ratio[i] = 100;
-				} else {
-					long hitVal = hit[i].get();
-					long missVal = miss[i].get();
-					long total = hitVal + missVal;
-					ratio[i] = total == 0 ? 0 : hitVal * 100 / total;
-				}
-			}
-			return ratio;
-		}
-
-		@Override
-		public long[] getEvictions() {
-			return getStatVals(statEvict);
-		}
-
-		private static AtomicLong[] newCounters() {
-			AtomicLong[] ret = new AtomicLong[PackExt.values().length];
-			for (int i = 0; i < ret.length; i++) {
-				ret[i] = new AtomicLong();
-			}
-			return ret;
-		}
-
-		private static long[] getStatVals(AtomicReference<AtomicLong[]> stat) {
-			AtomicLong[] stats = stat.get();
-			long[] cnt = new long[stats.length];
-			for (int i = 0; i < stats.length; i++) {
-				cnt[i] = stats[i].get();
-			}
-			return cnt;
-		}
-
-		private static AtomicLong getStat(AtomicReference<AtomicLong[]> stats,
-				DfsStreamKey key) {
-			int pos = key.packExtPos;
-			while (true) {
-				AtomicLong[] vals = stats.get();
-				if (pos < vals.length) {
-					return vals[pos];
-				}
-				AtomicLong[] expect = vals;
-				vals = new AtomicLong[Math.max(pos + 1,
-						PackExt.values().length)];
-				System.arraycopy(expect, 0, vals, 0, expect.length);
-				for (int i = expect.length; i < vals.length; i++) {
-					vals[i] = new AtomicLong();
-				}
-				if (stats.compareAndSet(expect, vals)) {
-					return vals[pos];
-				}
-			}
-		}
-	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
index a177669..e6068a1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
@@ -18,13 +18,13 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
 import static org.eclipse.jgit.internal.storage.dfs.DfsPackCompactor.configureReftable;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.COMMIT_GRAPH;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
 import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -100,6 +100,7 @@ public class DfsGarbageCollector {
 	private Set<ObjectId> allTags;
 	private Set<ObjectId> nonHeads;
 	private Set<ObjectId> tagTargets;
+	private Instant refLogExpire;
 
 	/**
 	 * Initialize a garbage collector.
@@ -200,6 +201,22 @@ public DfsGarbageCollector setReftableInitialMinUpdateIndex(long u) {
 		return this;
 	}
 
+
+	/**
+	 *  Set time limit to the reflog history.
+         *  <p>
+         *  Garbage Collector prunes entries from reflog history older than {@code refLogExpire}
+         *  <p>
+	 *
+	 * @param refLogExpire
+	 *            instant in time which defines refLog expiration
+	 * @return {@code this}
+	 */
+	public DfsGarbageCollector setRefLogExpire(Instant refLogExpire) {
+		this.refLogExpire = refLogExpire;
+		return this;
+	}
+
 	/**
 	 * Set maxUpdateIndex for the initial reftable created during conversion.
 	 *
@@ -687,14 +704,7 @@ private DfsPackDescription writePack(PackSource source, PackWriter pw,
 			pack.setBlockSize(PACK, out.blockSize());
 		}
 
-		try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) {
-			CountingOutputStream cnt = new CountingOutputStream(out);
-			pw.writeIndex(cnt);
-			pack.addFileExt(INDEX);
-			pack.setFileSize(INDEX, cnt.getCount());
-			pack.setBlockSize(INDEX, out.blockSize());
-			pack.setIndexVersion(pw.getIndexVersion());
-		}
+		pw.writeIndex(objdb.getPackIndexWriter(pack, pw.getIndexVersion()));
 
 		if (source != UNREACHABLE_GARBAGE && packConfig.getMinBytesForObjSizeIndex() >= 0) {
 			try (DfsOutputStream out = objdb.writeFile(pack,
@@ -741,6 +751,10 @@ private void writeReftable(DfsPackDescription pack) throws IOException {
 			compact.addAll(stack.readers());
 			compact.setIncludeDeletes(includeDeletes);
 			compact.setConfig(configureReftable(reftableConfig, out));
+			if(refLogExpire != null ){
+				compact.setReflogExpireOldestReflogTimeMillis(
+						refLogExpire.toEpochMilli());
+			}
 			compact.compact();
 			pack.addFileExt(REFTABLE);
 			pack.setReftableStats(compact.getStats());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
index a07d841..16315bf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
@@ -41,12 +41,13 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackIndex;
-import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexWriter;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
@@ -54,7 +55,6 @@
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.ObjectStream;
-import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.PackedObjectInfo;
 import org.eclipse.jgit.util.BlockList;
 import org.eclipse.jgit.util.IO;
@@ -71,6 +71,8 @@ public class DfsInserter extends ObjectInserter {
 	private static final int INDEX_VERSION = 2;
 
 	final DfsObjDatabase db;
+
+	private final int minBytesForObjectSizeIndex;
 	int compression = Deflater.BEST_COMPRESSION;
 
 	List<PackedObjectInfo> objectList;
@@ -83,8 +85,6 @@ public class DfsInserter extends ObjectInserter {
 	private boolean rollback;
 	private boolean checkExisting = true;
 
-	private int minBytesForObjectSizeIndex = -1;
-
 	/**
 	 * Initialize a new inserter.
 	 *
@@ -93,8 +93,9 @@ public class DfsInserter extends ObjectInserter {
 	 */
 	protected DfsInserter(DfsObjDatabase db) {
 		this.db = db;
-		PackConfig pc = new PackConfig(db.getRepository().getConfig());
-		this.minBytesForObjectSizeIndex = pc.getMinBytesForObjSizeIndex();
+		this.minBytesForObjectSizeIndex = db.getRepository().getConfig().getInt(
+				ConfigConstants.CONFIG_PACK_SECTION,
+				ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, -1);
 	}
 
 	/**
@@ -112,21 +113,6 @@ public void checkExisting(boolean check) {
 	void setCompressionLevel(int compression) {
 		this.compression = compression;
 	}
-
-	/**
-	 * Set minimum size for an object to be included in the object size index.
-	 *
-	 * <p>
-	 * Use 0 for all and -1 for nothing (the pack won't have object size index).
-	 *
-	 * @param minBytes
-	 *            only objects with size bigger or equal to this are included in
-	 *            the index.
-	 */
-	protected void setMinBytesForObjectSizeIndex(int minBytes) {
-		this.minBytesForObjectSizeIndex = minBytes;
-	}
-
 	@Override
 	public DfsPackParser newPackParser(InputStream in) throws IOException {
 		return new DfsPackParser(db, this, in);
@@ -333,7 +319,7 @@ PackIndex writePackIndex(DfsPackDescription pack, byte[] packHash,
 
 	private static void index(OutputStream out, byte[] packHash,
 			List<PackedObjectInfo> list) throws IOException {
-		PackIndexWriter.createVersion(out, INDEX_VERSION).write(list, packHash);
+		BasePackIndexWriter.createVersion(out, INDEX_VERSION).write(list, packHash);
 	}
 
 	void writeObjectSizeIndex(DfsPackDescription pack,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
index 616563f..efd666f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
@@ -12,6 +12,7 @@
 
 import static java.util.stream.Collectors.joining;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -27,7 +28,9 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndexWriterV1;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.internal.storage.pack.PackBitmapIndexWriter;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -771,4 +774,35 @@ public PackBitmapIndexWriter getPackBitmapIndexWriter(
 			}
 		};
 	}
+
+	/**
+	 * Returns a writer to store the pack index in this object database.
+	 *
+	 * @param pack
+	 *            Pack file to which the index is associated.
+	 * @param indexVersion
+	 *            which version of the index to write
+	 * @return a writer to store the index associated with the pack
+	 * @throws IOException
+	 *             when some I/O problem occurs while creating or writing to
+	 *             output stream
+	 */
+	public PackIndexWriter getPackIndexWriter(
+			DfsPackDescription pack, int indexVersion)
+			throws IOException {
+		return (objectsToStore, packDataChecksum) -> {
+			try (DfsOutputStream out = writeFile(pack, INDEX);
+					CountingOutputStream cnt = new CountingOutputStream(out)) {
+				final PackIndexWriter iw = BasePackIndexWriter
+						.createVersion(cnt,
+						indexVersion);
+				iw.write(objectsToStore, packDataChecksum);
+				pack.addFileExt(INDEX);
+				pack.setFileSize(INDEX, cnt.getCount());
+				pack.setBlockSize(INDEX, out.blockSize());
+				pack.setIndexVersion(indexVersion);
+			}
+		};
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
index 86144b3..f9c01b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
@@ -12,7 +12,7 @@
 
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
 import static org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation.PACK_DELTA;
@@ -249,6 +249,7 @@ private void compactPacks(DfsReader ctx, ProgressMonitor pm)
 			try {
 				writePack(objdb, outDesc, pw, pm);
 				writeIndex(objdb, outDesc, pw);
+				writeObjectSizeIndex(objdb, outDesc, pw);
 
 				PackStatistics stats = pw.getStatistics();
 
@@ -458,13 +459,20 @@ private static void writePack(DfsObjDatabase objdb,
 	private static void writeIndex(DfsObjDatabase objdb,
 			DfsPackDescription pack,
 			PackWriter pw) throws IOException {
-		try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) {
+		pw.writeIndex(objdb.getPackIndexWriter(pack, pw.getIndexVersion()));
+	}
+
+	private static void writeObjectSizeIndex(DfsObjDatabase objdb,
+											 DfsPackDescription pack,
+											 PackWriter pw) throws IOException {
+		try (DfsOutputStream out = objdb.writeFile(pack, OBJECT_SIZE_INDEX)) {
 			CountingOutputStream cnt = new CountingOutputStream(out);
-			pw.writeIndex(cnt);
-			pack.addFileExt(INDEX);
-			pack.setFileSize(INDEX, cnt.getCount());
-			pack.setBlockSize(INDEX, out.blockSize());
-			pack.setIndexVersion(pw.getIndexVersion());
+			pw.writeObjectSizeIndex(cnt);
+			if (cnt.getCount() > 0) {
+				pack.addFileExt(OBJECT_SIZE_INDEX);
+				pack.setFileSize(OBJECT_SIZE_INDEX, cnt.getCount());
+				pack.setBlockSize(OBJECT_SIZE_INDEX, out.blockSize());
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
index 9cfcbaa..62f6753 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
@@ -511,18 +511,15 @@ public long getObjectSize(AnyObjectId objectId, int typeHint)
 			throw new MissingObjectException(objectId.copy(), typeHint);
 		}
 
-		if (typeHint != Constants.OBJ_BLOB || !pack.hasObjectSizeIndex(this)) {
+		if (typeHint != Constants.OBJ_BLOB || !safeHasObjectSizeIndex(pack)) {
 			return pack.getObjectSize(this, objectId);
 		}
 
-		long sz = pack.getIndexedObjectSize(this, objectId);
+		Optional<Long> maybeSz = safeGetIndexedObjectSize(pack, objectId);
+		long sz = maybeSz.orElse(-1L);
 		if (sz >= 0) {
-			stats.objectSizeIndexHit += 1;
 			return sz;
 		}
-
-		// Object wasn't in the index
-		stats.objectSizeIndexMiss += 1;
 		return pack.getObjectSize(this, objectId);
 	}
 
@@ -541,23 +538,61 @@ public boolean isNotLargerThan(AnyObjectId objectId, int typeHint,
 		}
 
 		stats.isNotLargerThanCallCount += 1;
-		if (typeHint != Constants.OBJ_BLOB || !pack.hasObjectSizeIndex(this)) {
+		if (typeHint != Constants.OBJ_BLOB || !safeHasObjectSizeIndex(pack)) {
 			return pack.getObjectSize(this, objectId) <= limit;
 		}
 
-		long sz = pack.getIndexedObjectSize(this, objectId);
+		Optional<Long> maybeSz = safeGetIndexedObjectSize(pack, objectId);
+		if (maybeSz.isEmpty()) {
+			// Exception in object size index
+			return pack.getObjectSize(this, objectId) <= limit;
+		}
+
+		long sz = maybeSz.get();
+		if (sz >= 0) {
+			return sz <= limit;
+		}
+
+		if (isLimitInsideIndexThreshold(pack, limit)) {
+			// With threshold T, not-found means object < T
+			// If limit L > T, then object < T < L
+			return true;
+		}
+
+		return pack.getObjectSize(this, objectId) <= limit;
+	}
+
+	private boolean safeHasObjectSizeIndex(DfsPackFile pack) {
+		try {
+			return pack.hasObjectSizeIndex(this);
+		} catch (IOException e) {
+			return false;
+		}
+	}
+
+	private Optional<Long> safeGetIndexedObjectSize(DfsPackFile pack,
+			AnyObjectId objectId) {
+		long sz;
+		try {
+			sz = pack.getIndexedObjectSize(this, objectId);
+		} catch (IOException e) {
+			// Do not count the exception as an index miss
+			return Optional.empty();
+		}
 		if (sz < 0) {
 			stats.objectSizeIndexMiss += 1;
 		} else {
 			stats.objectSizeIndexHit += 1;
 		}
+		return Optional.of(sz);
+	}
 
-		// Got size from index or we didn't but we are sure it should be there.
-		if (sz >= 0 || pack.getObjectSizeIndexThreshold(this) <= limit) {
-			return sz <= limit;
+	private boolean isLimitInsideIndexThreshold(DfsPackFile pack, long limit) {
+		try {
+			return pack.getObjectSizeIndexThreshold(this) <= limit;
+		} catch (IOException e) {
+			return false;
 		}
-
-		return pack.getObjectSize(this, objectId) <= limit;
 	}
 
 	private DfsPackFile findPackWithObject(AnyObjectId objectId)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
index 5f5e819..c397469 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
@@ -13,6 +13,7 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DELTA_BASE_CACHE_LIMIT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_BUFFER;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_THRESHOLD;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_USE_OBJECT_SIZE_INDEX;
@@ -197,6 +198,9 @@ public DfsReaderOptions fromConfig(Config rc) {
 		setUseObjectSizeIndex(rc.getBoolean(CONFIG_CORE_SECTION,
 				CONFIG_DFS_SECTION, CONFIG_KEY_USE_OBJECT_SIZE_INDEX,
 				false));
+		setLoadRevIndexInParallel(
+				rc.getBoolean(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+						CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL, false));
 		return this;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java
index 858f731..bb44f93 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java
@@ -37,6 +37,11 @@
  * type.
  */
 class PackExtBlockCacheTable implements DfsBlockCacheTable {
+	/**
+	 * Table name.
+	 */
+	private final String name;
+
 	private final DfsBlockCacheTable defaultBlockCacheTable;
 
 	// Holds the unique tables backing the extBlockCacheTables values.
@@ -120,13 +125,19 @@ static PackExtBlockCacheTable fromCacheTables(
 		Set<DfsBlockCacheTable> blockCacheTables = new HashSet<>();
 		blockCacheTables.add(defaultBlockCacheTable);
 		blockCacheTables.addAll(packExtBlockCacheTables.values());
-		return new PackExtBlockCacheTable(defaultBlockCacheTable,
+		String name = defaultBlockCacheTable.getName() + "," //$NON-NLS-1$
+				+ packExtBlockCacheTables.values().stream()
+						.map(DfsBlockCacheTable::getName).sorted()
+						.collect(Collectors.joining(",")); //$NON-NLS-1$
+		return new PackExtBlockCacheTable(name, defaultBlockCacheTable,
 				List.copyOf(blockCacheTables), packExtBlockCacheTables);
 	}
 
-	private PackExtBlockCacheTable(DfsBlockCacheTable defaultBlockCacheTable,
+	private PackExtBlockCacheTable(String name,
+			DfsBlockCacheTable defaultBlockCacheTable,
 			List<DfsBlockCacheTable> blockCacheTableList,
 			Map<PackExt, DfsBlockCacheTable> extBlockCacheTables) {
+		this.name = name;
 		this.defaultBlockCacheTable = defaultBlockCacheTable;
 		this.blockCacheTableList = blockCacheTableList;
 		this.extBlockCacheTables = extBlockCacheTables;
@@ -177,10 +188,15 @@ public <T> T get(DfsStreamKey key, long position) {
 	}
 
 	@Override
-	public BlockCacheStats getBlockCacheStats() {
-		return new CacheStats(blockCacheTableList.stream()
-				.map(DfsBlockCacheTable::getBlockCacheStats)
-				.collect(Collectors.toList()));
+	public List<BlockCacheStats> getBlockCacheStats() {
+		return blockCacheTableList.stream()
+				.flatMap(cacheTable -> cacheTable.getBlockCacheStats().stream())
+				.collect(Collectors.toList());
+	}
+
+	@Override
+	public String getName() {
+		return name;
 	}
 
 	private DfsBlockCacheTable getTable(PackExt packExt) {
@@ -196,94 +212,4 @@ private DfsBlockCacheTable getTable(DfsStreamKey key) {
 	private static PackExt getPackExt(DfsStreamKey key) {
 		return PackExt.values()[key.packExtPos];
 	}
-
-	private static class CacheStats implements BlockCacheStats {
-		private final List<BlockCacheStats> blockCacheStats;
-
-		private CacheStats(List<BlockCacheStats> blockCacheStats) {
-			this.blockCacheStats = blockCacheStats;
-		}
-
-		@Override
-		public long[] getCurrentSize() {
-			long[] sums = emptyPackStats();
-			for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
-				sums = add(sums, blockCacheStatsEntry.getCurrentSize());
-			}
-			return sums;
-		}
-
-		@Override
-		public long[] getHitCount() {
-			long[] sums = emptyPackStats();
-			for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
-				sums = add(sums, blockCacheStatsEntry.getHitCount());
-			}
-			return sums;
-		}
-
-		@Override
-		public long[] getMissCount() {
-			long[] sums = emptyPackStats();
-			for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
-				sums = add(sums, blockCacheStatsEntry.getMissCount());
-			}
-			return sums;
-		}
-
-		@Override
-		public long[] getTotalRequestCount() {
-			long[] sums = emptyPackStats();
-			for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
-				sums = add(sums, blockCacheStatsEntry.getTotalRequestCount());
-			}
-			return sums;
-		}
-
-		@Override
-		public long[] getHitRatio() {
-			long[] hit = getHitCount();
-			long[] miss = getMissCount();
-			long[] ratio = new long[Math.max(hit.length, miss.length)];
-			for (int i = 0; i < ratio.length; i++) {
-				if (i >= hit.length) {
-					ratio[i] = 0;
-				} else if (i >= miss.length) {
-					ratio[i] = 100;
-				} else {
-					long total = hit[i] + miss[i];
-					ratio[i] = total == 0 ? 0 : hit[i] * 100 / total;
-				}
-			}
-			return ratio;
-		}
-
-		@Override
-		public long[] getEvictions() {
-			long[] sums = emptyPackStats();
-			for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
-				sums = add(sums, blockCacheStatsEntry.getEvictions());
-			}
-			return sums;
-		}
-
-		private static long[] emptyPackStats() {
-			return new long[PackExt.values().length];
-		}
-
-		private static long[] add(long[] first, long[] second) {
-			long[] sums = new long[Integer.max(first.length, second.length)];
-			int i;
-			for (i = 0; i < Integer.min(first.length, second.length); i++) {
-				sums[i] = first[i] + second[i];
-			}
-			for (int j = i; j < first.length; j++) {
-				sums[j] = first[i];
-			}
-			for (int j = i; j < second.length; j++) {
-				sums[j] = second[i];
-			}
-			return sums;
-		}
-	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackIndexWriter.java
similarity index 97%
rename from org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackIndexWriter.java
index 87e0b44..b89cc1e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackIndexWriter.java
@@ -19,6 +19,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.transport.PackedObjectInfo;
 import org.eclipse.jgit.util.NB;
@@ -31,7 +32,7 @@
  * random access to any object in the pack by associating an ObjectId to the
  * byte offset within the pack where the object's data can be read.
  */
-public abstract class PackIndexWriter {
+public abstract class BasePackIndexWriter implements PackIndexWriter {
 	/** Magic constant indicating post-version 1 format. */
 	protected static final byte[] TOC = { -1, 't', 'O', 'c' };
 
@@ -147,7 +148,7 @@ public static PackIndexWriter createVersion(final OutputStream dst,
 	 *            the stream this instance outputs to. If not already buffered
 	 *            it will be automatically wrapped in a buffered stream.
 	 */
-	protected PackIndexWriter(OutputStream dst) {
+	protected BasePackIndexWriter(OutputStream dst) {
 		out = new DigestOutputStream(dst instanceof BufferedOutputStream ? dst
 				: new BufferedOutputStream(dst),
 				Constants.newMessageDigest());
@@ -172,6 +173,7 @@ protected PackIndexWriter(OutputStream dst) {
 	 *             an error occurred while writing to the output stream, or this
 	 *             index format cannot store the object data supplied.
 	 */
+	@Override
 	public void write(final List<? extends PackedObjectInfo> toStore,
 			final byte[] packDataChecksum) throws IOException {
 		entries = toStore;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
index 80240e5..25b7583 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
@@ -28,8 +28,10 @@
 import java.util.stream.Collectors;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.PackRefsCommand;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.events.RefsChangedEvent;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
 import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate;
 import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase;
@@ -39,6 +41,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefRename;
@@ -108,6 +111,22 @@ public boolean hasFastTipsWithSha1() throws IOException {
 	}
 
 	/**
+	 * {@inheritDoc}
+	 *
+	 * For Reftable, all the data is compacted into a single table.
+	 */
+	@Override
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		pm.beginTask(JGitText.get().packRefs, 1);
+		try {
+			compactFully();
+		} finally {
+			pm.endTask();
+		}
+	}
+
+	/**
 	 * Runs a full compaction for GC purposes.
 	 * @throws IOException on I/O errors
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
index b5d29a3..84c8565 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
@@ -33,6 +33,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.attributes.AttributesNode;
 import org.eclipse.jgit.attributes.AttributesNodeProvider;
@@ -60,7 +61,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
-import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IO;
@@ -595,13 +595,12 @@ private boolean shouldAutoDetach() {
 	@Override
 	public void autoGC(ProgressMonitor monitor) {
 		GC gc = new GC(this);
-		gc.setPackConfig(new PackConfig(this));
 		gc.setProgressMonitor(monitor);
 		gc.setAuto(true);
 		gc.setBackground(shouldAutoDetach());
 		try {
 			gc.gc();
-		} catch (ParseException | IOException e) {
+		} catch (ParseException | IOException | GitAPIException e) {
 			throw new JGitInternalException(JGitText.get().gcFailed, e);
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 67b43cb..3105a3a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -63,6 +63,8 @@
 import java.util.stream.Stream;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CancelledException;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -233,9 +235,11 @@ public GC(FileRepository repo) {
 	 * @throws java.text.ParseException
 	 *             If the configuration parameter "gc.pruneexpire" couldn't be
 	 *             parsed
+	 * @throws GitAPIException
+	 *             If packing refs failed
 	 */
 	public CompletableFuture<Collection<Pack>> gc()
-			throws IOException, ParseException {
+			throws IOException, ParseException, GitAPIException {
 		if (!background) {
 			return CompletableFuture.completedFuture(doGc());
 		}
@@ -254,7 +258,7 @@ public CompletableFuture<Collection<Pack>> gc()
 					gcLog.commit();
 				}
 				return newPacks;
-			} catch (IOException | ParseException e) {
+			} catch (IOException | ParseException | GitAPIException e) {
 				try {
 					gcLog.write(e.getMessage());
 					StringWriter sw = new StringWriter();
@@ -277,7 +281,8 @@ private ExecutorService executor() {
 		return (executor != null) ? executor : WorkQueue.getExecutor();
 	}
 
-	private Collection<Pack> doGc() throws IOException, ParseException {
+	private Collection<Pack> doGc()
+			throws IOException, ParseException, GitAPIException {
 		if (automatic && !needGc()) {
 			return Collections.emptyList();
 		}
@@ -286,7 +291,8 @@ private Collection<Pack> doGc() throws IOException, ParseException {
 				return Collections.emptyList();
 			}
 			pm.start(6 /* tasks */);
-			packRefs();
+			new PackRefsCommand(repo).setProgressMonitor(pm).setAll(true)
+					.call();
 			// TODO: implement reflog_expire(pm, repo);
 			Collection<Pack> newPacks = repack();
 			prune(Collections.emptySet());
@@ -780,43 +786,6 @@ private static boolean equals(Ref r1, Ref r2) {
 	}
 
 	/**
-	 * Pack ref storage. For a RefDirectory database, this packs all
-	 * non-symbolic, loose refs into packed-refs. For Reftable, all of the data
-	 * is compacted into a single table.
-	 *
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 */
-	public void packRefs() throws IOException {
-		RefDatabase refDb = repo.getRefDatabase();
-		if (refDb instanceof FileReftableDatabase) {
-			// TODO: abstract this more cleanly.
-			pm.beginTask(JGitText.get().packRefs, 1);
-			try {
-				((FileReftableDatabase) refDb).compactFully();
-			} finally {
-				pm.endTask();
-			}
-			return;
-		}
-
-		Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS);
-		List<String> refsToBePacked = new ArrayList<>(refs.size());
-		pm.beginTask(JGitText.get().packRefs, refs.size());
-		try {
-			for (Ref ref : refs) {
-				checkCancelled();
-				if (!ref.isSymbolic() && ref.getStorage().isLoose())
-					refsToBePacked.add(ref.getName());
-				pm.update(1);
-			}
-			((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked);
-		} finally {
-			pm.endTask();
-		}
-	}
-
-	/**
 	 * Packs all objects which reachable from any of the heads into one pack
 	 * file. Additionally all objects which are not reachable from any head but
 	 * which are reachable from any of the other refs (e.g. tags), special refs
@@ -1505,7 +1474,7 @@ public static class RepoStatistics {
 		public long numberOfPackFiles;
 
 		/**
-		 * The number of pack files that were created after the last bitmap
+		 * The number of pack files that were created since the last bitmap
 		 * generation.
 		 */
 		public long numberOfPackFilesSinceBitmap;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
index 9f27f4b..746e124 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
@@ -28,6 +28,7 @@
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig;
@@ -110,7 +111,7 @@ public class ObjectDirectoryPackParser extends PackParser {
 	 * @param version
 	 *            the version to write. The special version 0 designates the
 	 *            oldest (most compatible) format available for the objects.
-	 * @see PackIndexWriter
+	 * @see BasePackIndexWriter
 	 */
 	public void setIndexVersion(int version) {
 		indexVersion = version;
@@ -386,9 +387,9 @@ private void writeIdx() throws IOException {
 		try (FileOutputStream os = new FileOutputStream(tmpIdx)) {
 			final PackIndexWriter iw;
 			if (indexVersion <= 0)
-				iw = PackIndexWriter.createOldestPossible(os, list);
+				iw = BasePackIndexWriter.createOldestPossible(os, list);
 			else
-				iw = PackIndexWriter.createVersion(os, indexVersion);
+				iw = BasePackIndexWriter.createVersion(os, indexVersion);
 			iw.write(list, packHash);
 			os.getChannel().force(true);
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
index c0540d5..7189ce2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
@@ -109,7 +109,7 @@ static PackIndex read(InputStream fd) throws IOException,
 	}
 
 	private static boolean isTOC(byte[] h) {
-		final byte[] toc = PackIndexWriter.TOC;
+		final byte[] toc = BasePackIndexWriter.TOC;
 		for (int i = 0; i < toc.length; i++)
 			if (h[i] != toc[i])
 				return false;
@@ -302,8 +302,9 @@ void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
 	 *
 	 */
 	class MutableEntry {
+		/** Buffer of the ObjectId visited by the EntriesIterator. */
 		final MutableObjectId idBuffer = new MutableObjectId();
-
+		/** Offset into the packfile of the current object. */
 		long offset;
 
 		/**
@@ -321,7 +322,6 @@ public long getOffset() {
 		 * @return hex string describing the object id of this entry.
 		 */
 		public String name() {
-			ensureId();
 			return idBuffer.name();
 		}
 
@@ -331,7 +331,6 @@ public String name() {
 		 * @return a copy of the object id.
 		 */
 		public ObjectId toObjectId() {
-			ensureId();
 			return idBuffer.toObjectId();
 		}
 
@@ -342,32 +341,33 @@ public ObjectId toObjectId() {
 		 */
 		public MutableEntry cloneEntry() {
 			final MutableEntry r = new MutableEntry();
-			ensureId();
 			r.idBuffer.fromObjectId(idBuffer);
 			r.offset = offset;
 			return r;
 		}
-
-		void ensureId() {
-			// Override in implementations.
-		}
 	}
 
 	/**
 	 * Base implementation of the iterator over index entries.
 	 */
 	abstract class EntriesIterator implements Iterator<MutableEntry> {
-		protected final MutableEntry entry = initEntry();
-
 		private final long objectCount;
 
+		private final MutableEntry entry = new MutableEntry();
+
+		/** Counts number of entries accessed so far. */
+		private long returnedNumber = 0;
+
+		/**
+		 * Construct an iterator that can move objectCount times forward.
+		 *
+		 * @param objectCount
+		 *            the number of objects in the PackFile.
+		 */
 		protected EntriesIterator(long objectCount) {
 			this.objectCount = objectCount;
 		}
 
-		protected long returnedNumber = 0;
-
-		protected abstract MutableEntry initEntry();
 
 		@Override
 		public boolean hasNext() {
@@ -379,7 +379,55 @@ public boolean hasNext() {
 		 * element.
 		 */
 		@Override
-		public abstract MutableEntry next();
+		public MutableEntry next() {
+			readNext();
+			returnedNumber++;
+			return entry;
+		}
+
+		/**
+		 * Used by subclasses to load the next entry into the MutableEntry.
+		 * <p>
+		 * Subclasses are expected to populate the entry with
+		 * {@link #setIdBuffer} and {@link #setOffset}.
+		 */
+		protected abstract void readNext();
+
+
+		/**
+		 * Copies to the entry an {@link ObjectId} from the int buffer and
+		 * position idx
+		 *
+		 * @param raw
+		 *            the raw data
+		 * @param idx
+		 *            the index into {@code raw}
+		 */
+		protected void setIdBuffer(int[] raw, int idx) {
+			entry.idBuffer.fromRaw(raw, idx);
+		}
+
+		/**
+		 * Copies to the entry an {@link ObjectId} from the byte array at
+		 * position idx.
+		 *
+		 * @param raw
+		 *            the raw data
+		 * @param idx
+		 *            the index into {@code raw}
+		 */
+		protected void setIdBuffer(byte[] raw, int idx) {
+			entry.idBuffer.fromRaw(raw, idx);
+		}
+
+		/**
+		 * Sets the {@code offset} to the entry
+		 *
+		 * @param offset the offset in the pack file
+		 */
+		protected void setOffset(long offset) {
+			entry.offset = offset;
+		}
 
 		@Override
 		public void remove() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java
index d7c8378..be48358 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java
@@ -203,7 +203,7 @@ public boolean hasCRC32Support() {
 
 	@Override
 	public Iterator<MutableEntry> iterator() {
-		return new IndexV1Iterator(objectCnt);
+		return new EntriesIteratorV1(this);
 	}
 
 	@Override
@@ -246,36 +246,30 @@ private static int idOffset(int mid) {
 		return packChecksum;
 	}
 
-	private class IndexV1Iterator extends EntriesIterator {
-		int levelOne;
+	private static class EntriesIteratorV1 extends EntriesIterator {
+		private int levelOne;
 
-		int levelTwo;
+		private int levelTwo;
 
-		IndexV1Iterator(long objectCount) {
-			super(objectCount);
+		private final PackIndexV1 packIndex;
+
+		private EntriesIteratorV1(PackIndexV1 packIndex) {
+			super(packIndex.objectCnt);
+			this.packIndex = packIndex;
 		}
 
 		@Override
-		protected MutableEntry initEntry() {
-			return new MutableEntry() {
-				@Override
-				protected void ensureId() {
-					idBuffer.fromRaw(idxdata[levelOne], levelTwo
-							- Constants.OBJECT_ID_LENGTH);
-				}
-			};
-		}
-
-		@Override
-		public MutableEntry next() {
-			for (; levelOne < idxdata.length; levelOne++) {
-				if (idxdata[levelOne] == null)
+		protected void readNext() {
+			for (; levelOne < packIndex.idxdata.length; levelOne++) {
+				if (packIndex.idxdata[levelOne] == null)
 					continue;
-				if (levelTwo < idxdata[levelOne].length) {
-					entry.offset = NB.decodeUInt32(idxdata[levelOne], levelTwo);
-					levelTwo += Constants.OBJECT_ID_LENGTH + 4;
-					returnedNumber++;
-					return entry;
+				if (levelTwo < packIndex.idxdata[levelOne].length) {
+					super.setOffset(NB.decodeUInt32(packIndex.idxdata[levelOne],
+							levelTwo));
+					this.levelTwo += Constants.OBJECT_ID_LENGTH + 4;
+					super.setIdBuffer(packIndex.idxdata[levelOne],
+							levelTwo - Constants.OBJECT_ID_LENGTH);
+					return;
 				}
 				levelTwo = 0;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java
index caf8b71..36e54fc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java
@@ -224,7 +224,7 @@ public boolean hasCRC32Support() {
 
 	@Override
 	public Iterator<MutableEntry> iterator() {
-		return new EntriesIteratorV2(objectCnt);
+		return new EntriesIteratorV2(this);
 	}
 
 	@Override
@@ -289,41 +289,34 @@ else if (cmp == 0) {
 		return packChecksum;
 	}
 
-	private class EntriesIteratorV2 extends EntriesIterator {
-		int levelOne;
+	private static class EntriesIteratorV2 extends EntriesIterator {
+		private int levelOne = 0;
 
-		int levelTwo;
+		private int levelTwo = 0;
 
-		EntriesIteratorV2(long objectCount){
-			super(objectCount);
+		private final PackIndexV2 packIndex;
+
+		private EntriesIteratorV2(PackIndexV2 packIndex) {
+			super(packIndex.objectCnt);
+			this.packIndex = packIndex;
 		}
 
 		@Override
-		protected MutableEntry initEntry() {
-			return new MutableEntry() {
-				@Override
-				protected void ensureId() {
-					idBuffer.fromRaw(names[levelOne], levelTwo
-							- Constants.OBJECT_ID_LENGTH / 4);
-				}
-			};
-		}
-
-		@Override
-		public MutableEntry next() {
-			for (; levelOne < names.length; levelOne++) {
-				if (levelTwo < names[levelOne].length) {
+		protected void readNext() {
+			for (; levelOne < packIndex.names.length; levelOne++) {
+				if (levelTwo < packIndex.names[levelOne].length) {
 					int idx = levelTwo / (Constants.OBJECT_ID_LENGTH / 4) * 4;
-					long offset = NB.decodeUInt32(offset32[levelOne], idx);
+					long offset = NB.decodeUInt32(packIndex.offset32[levelOne],
+							idx);
 					if ((offset & IS_O64) != 0) {
 						idx = (8 * (int) (offset & ~IS_O64));
-						offset = NB.decodeUInt64(offset64, idx);
+						offset = NB.decodeUInt64(packIndex.offset64, idx);
 					}
-					entry.offset = offset;
-
-					levelTwo += Constants.OBJECT_ID_LENGTH / 4;
-					returnedNumber++;
-					return entry;
+					super.setOffset(offset);
+					this.levelTwo += Constants.OBJECT_ID_LENGTH / 4;
+					super.setIdBuffer(packIndex.names[levelOne],
+							levelTwo - Constants.OBJECT_ID_LENGTH / 4);
+					return;
 				}
 				levelTwo = 0;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java
index 7e28b5e..f0b6193 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java
@@ -21,10 +21,10 @@
 /**
  * Creates the version 1 (old style) pack table of contents files.
  *
- * @see PackIndexWriter
+ * @see BasePackIndexWriter
  * @see PackIndexV1
  */
-class PackIndexWriterV1 extends PackIndexWriter {
+class PackIndexWriterV1 extends BasePackIndexWriter {
 	static boolean canStore(PackedObjectInfo oe) {
 		// We are limited to 4 GB per pack as offset is 32 bit unsigned int.
 		//
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java
index fc5ef61..b72b35a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java
@@ -19,10 +19,10 @@
 /**
  * Creates the version 2 pack table of contents files.
  *
- * @see PackIndexWriter
+ * @see BasePackIndexWriter
  * @see PackIndexV2
  */
-class PackIndexWriterV2 extends PackIndexWriter {
+class PackIndexWriterV2 extends BasePackIndexWriter {
 	private static final int MAX_OFFSET_32 = 0x7fffffff;
 	private static final int IS_OFFSET_64 = 0x80000000;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
index 1b092a3..55e047b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
@@ -77,6 +77,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -320,7 +321,8 @@ public void flush() throws IOException {
 	private static void writePackIndex(File idx, byte[] packHash,
 			List<PackedObjectInfo> list) throws IOException {
 		try (OutputStream os = new FileOutputStream(idx)) {
-			PackIndexWriter w = PackIndexWriter.createVersion(os, INDEX_VERSION);
+			PackIndexWriter w = BasePackIndexWriter.createVersion(os,
+					INDEX_VERSION);
 			w.write(list, packHash);
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java
index ef9753c..720a3bc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java
@@ -75,20 +75,20 @@ long findNextOffset(long offset, long maxOffset)
 			throws CorruptObjectException;
 
 	/**
-	 * Find the position in the primary index of the object at the given pack
+	 * Find the position in the reverse index of the object at the given pack
 	 * offset.
 	 *
 	 * @param offset
 	 *            the pack offset of the object
-	 * @return the position in the primary index of the object
+	 * @return the position in the reverse index of the object
 	 */
 	int findPosition(long offset);
 
 	/**
-	 * Find the object that is in the given position in the primary index.
+	 * Find the object that is in the given position in the reverse index.
 	 *
 	 * @param nthPosition
-	 *            the position of the object in the primary index
+	 *            the position of the object in the reverse index
 	 * @return the object in that position
 	 */
 	ObjectId findObjectByPosition(int nthPosition);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index cc48176..a1b7a9e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -59,6 +59,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.PackRefsCommand;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -71,6 +72,7 @@
 import org.eclipse.jgit.lib.CoreConfig.TrustPackedRefsStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefComparator;
 import org.eclipse.jgit.lib.RefDatabase;
@@ -292,6 +294,33 @@ public void refresh() {
 		clearReferences();
 	}
 
+	/**
+	 * {@inheritDoc}
+	 *
+	 * For a RefDirectory database, by default this packs non-symbolic, loose
+	 * tag refs into packed-refs. If {@code all} flag is set, this packs all the
+	 * non-symbolic, loose refs.
+	 */
+	@Override
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		String prefix = packRefs.isAll() ? R_REFS : R_TAGS;
+		Collection<Ref> refs = getRefsByPrefix(prefix);
+		List<String> refsToBePacked = new ArrayList<>(refs.size());
+		pm.beginTask(JGitText.get().packRefs, refs.size());
+		try {
+			for (Ref ref : refs) {
+				if (!ref.isSymbolic() && ref.getStorage().isLoose()) {
+					refsToBePacked.add(ref.getName());
+				}
+				pm.update(1);
+			}
+			pack(refsToBePacked);
+		} finally {
+			pm.endTask();
+		}
+	}
+
 	@Override
 	public boolean isNameConflicting(String name) throws IOException {
 		// Cannot be nested within an existing reference.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
index 01f514b..11c4547 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
@@ -205,7 +205,7 @@ public void writeObjects(PackOutputStream out, List<ObjectToPack> list)
 	 * @param cnt
 	 *            number of bytes to copy. This value may exceed the number of
 	 *            bytes remaining in the window starting at offset
-	 *            <code>pos</code>.
+	 *            <code>position</code>.
 	 * @return number of bytes actually copied; this may be less than
 	 *         <code>cnt</code> if <code>cnt</code> exceeded the number of bytes
 	 *         available.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java
new file mode 100644
index 0000000..f69e68d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024, Google LLC.
+ *
+ * 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.internal.storage.pack;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.transport.PackedObjectInfo;
+
+/**
+ * Represents a function that accepts a collection of objects to write into a
+ * primary pack index storage format.
+ */
+public interface PackIndexWriter {
+	/**
+	 * Write all object entries to the index stream.
+	 *
+	 * @param toStore
+	 *            sorted list of objects to store in the index. The caller must
+	 *            have previously sorted the list using
+	 *            {@link org.eclipse.jgit.transport.PackedObjectInfo}'s native
+	 *            {@link java.lang.Comparable} implementation.
+	 * @param packDataChecksum
+	 *            checksum signature of the entire pack data content. This is
+	 *            traditionally the last 20 bytes of the pack file's own stream.
+	 * @throws java.io.IOException
+	 *             an error occurred while writing to the output stream, or the
+	 *             underlying format cannot store the object data supplied.
+	 */
+	void write(List<? extends PackedObjectInfo> toStore,
+			byte[] packDataChecksum) throws IOException;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
index ca4598d..27fb814 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
@@ -58,10 +58,10 @@
 import org.eclipse.jgit.errors.SearchForReuseTimeout;
 import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException;
 import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
+import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder;
 import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackReverseIndexWriter;
-import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.AsyncObjectSizeQueue;
 import org.eclipse.jgit.lib.BatchingProgressMonitor;
@@ -118,7 +118,7 @@
  * {@link #preparePack(ProgressMonitor, Set, Set)}, and streaming with
  * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. If the
  * pack is being stored as a file the matching index can be written out after
- * writing the pack by {@link #writeIndex(OutputStream)}. An optional bitmap
+ * writing the pack by {@link #writeIndex(PackIndexWriter)}. An optional bitmap
  * index can be made by calling {@link #prepareBitmapIndex(ProgressMonitor)}
  * followed by {@link #writeBitmapIndex(PackBitmapIndexWriter)}.
  * </p>
@@ -303,19 +303,6 @@ public PackWriter(Repository repo) {
 	}
 
 	/**
-	 * Create a writer to load objects from the specified reader.
-	 * <p>
-	 * Objects for packing are specified in {@link #preparePack(Iterator)} or
-	 * {@link #preparePack(ProgressMonitor, Set, Set)}.
-	 *
-	 * @param reader
-	 *            reader to read from the repository with.
-	 */
-	public PackWriter(ObjectReader reader) {
-		this(new PackConfig(), reader);
-	}
-
-	/**
 	 * Create writer for specified repository.
 	 * <p>
 	 * Objects for packing are specified in {@link #preparePack(Iterator)} or
@@ -1091,7 +1078,7 @@ public int getIndexVersion() {
 		if (indexVersion <= 0) {
 			for (BlockList<ObjectToPack> objs : objectsLists)
 				indexVersion = Math.max(indexVersion,
-						PackIndexWriter.oldestPossibleFormat(objs));
+						BasePackIndexWriter.oldestPossibleFormat(objs));
 		}
 		return indexVersion;
 	}
@@ -1112,12 +1099,28 @@ public int getIndexVersion() {
 	 *             the index data could not be written to the supplied stream.
 	 */
 	public void writeIndex(OutputStream indexStream) throws IOException {
+		writeIndex(BasePackIndexWriter.createVersion(indexStream,
+				getIndexVersion()));
+	}
+
+	/**
+	 * Create an index file to match the pack file just written.
+	 * <p>
+	 * Called after
+	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
+	 * <p>
+	 * Writing an index is only required for local pack storage. Packs sent on
+	 * the network do not need to create an index.
+	 *
+	 * @param iw
+	 *            an {@link PackIndexWriter} instance to write the index
+	 * @throws java.io.IOException
+	 *             the index data could not be written to the supplied stream.
+	 */
+	public void writeIndex(PackIndexWriter iw) throws IOException {
 		if (isIndexDisabled())
 			throw new IOException(JGitText.get().cachedPacksPreventsIndexCreation);
-
 		long writeStart = System.currentTimeMillis();
-		final PackIndexWriter iw = PackIndexWriter.createVersion(
-				indexStream, getIndexVersion());
 		iw.write(sortByName(), packcsum);
 		stats.timeWriting += System.currentTimeMillis() - writeStart;
 	}
@@ -2498,7 +2501,7 @@ private final boolean have(ObjectToPack ptr, AnyObjectId objectId) {
 	 * object graph at selected commits. Writing a bitmap index is an optional
 	 * feature that not all pack users may require.
 	 * <p>
-	 * Called after {@link #writeIndex(OutputStream)}.
+	 * Called after {@link #writeIndex(PackIndexWriter)}.
 	 * <p>
 	 * To reduce memory internal state is cleared during this method, rendering
 	 * the PackWriter instance useless for anything further than a call to write
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
index 3e75a9d..542d6e9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
@@ -427,7 +427,35 @@ protected List<String> validate(String key, List<String> value) {
 		return value;
 	}
 
-	private static boolean patternMatchesHost(String pattern, String name) {
+	/**
+	 * Tells whether a given {@code name} matches the given list of patterns,
+	 * accounting for negative matches.
+	 *
+	 * @param patterns
+	 *            to test {@code name} against; any pattern starting with an
+	 *            exclamation mark is a negative pattern
+	 * @param name
+	 *            to test
+	 * @return {@code true} if the {@code name} matches at least one of the
+	 *         non-negative patterns and none of the negative patterns,
+	 *         {@code false} otherwise
+	 * @since 7.1
+	 */
+	public static boolean patternMatch(Iterable<String> patterns, String name) {
+		boolean doesMatch = false;
+		for (String pattern : patterns) {
+			if (pattern.startsWith("!")) { //$NON-NLS-1$
+				if (patternMatches(pattern.substring(1), name)) {
+					return false;
+				}
+			} else if (!doesMatch && patternMatches(pattern, name)) {
+				doesMatch = true;
+			}
+		}
+		return doesMatch;
+	}
+
+	private static boolean patternMatches(String pattern, String name) {
 		if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
 			final FileNameMatcher fn;
 			try {
@@ -680,18 +708,7 @@ public HostEntry(List<String> patterns) {
 		}
 
 		boolean matches(String hostName) {
-			boolean doesMatch = false;
-			for (String pattern : patterns) {
-				if (pattern.startsWith("!")) { //$NON-NLS-1$
-					if (patternMatchesHost(pattern.substring(1), hostName)) {
-						return false;
-					}
-				} else if (!doesMatch
-						&& patternMatchesHost(pattern, hostName)) {
-					doesMatch = true;
-				}
-			}
-			return doesMatch;
+			return patternMatch(patterns, hostName);
 		}
 
 		private static String toKey(String key) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
index d232be6..0c1da83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -15,6 +15,7 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WORKTREE;
 import static org.eclipse.jgit.lib.Constants.CONFIG;
 import static org.eclipse.jgit.lib.Constants.DOT_GIT;
+import static org.eclipse.jgit.lib.Constants.GITDIR_FILE;
 import static org.eclipse.jgit.lib.Constants.GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_CEILING_DIRECTORIES_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_COMMON_DIR_KEY;
@@ -23,7 +24,6 @@
 import static org.eclipse.jgit.lib.Constants.GIT_OBJECT_DIRECTORY_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_WORK_TREE_KEY;
 import static org.eclipse.jgit.lib.Constants.OBJECTS;
-import static org.eclipse.jgit.lib.Constants.GITDIR_FILE;
 
 import java.io.File;
 import java.io.IOException;
@@ -485,7 +485,7 @@ public B readEnvironment(SystemReader sr) {
 		if (getAlternateObjectDirectories() == null) {
 			String val = sr.getenv(GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY);
 			if (val != null) {
-				for (String path : val.split(File.pathSeparator))
+				for (String path : val.split(File.pathSeparator, -1))
 					addAlternateObjectDirectory(new File(path));
 			}
 		}
@@ -505,7 +505,7 @@ public B readEnvironment(SystemReader sr) {
 		if (ceilingDirectories == null) {
 			String val = sr.getenv(GIT_CEILING_DIRECTORIES_KEY);
 			if (val != null) {
-				for (String path : val.split(File.pathSeparator))
+				for (String path : val.split(File.pathSeparator, -1))
 					addCeilingDirectory(new File(path));
 			}
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index acb54d7..a57f1b7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -206,7 +206,36 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_SIGNINGKEY = "signingKey";
 
 	/**
+	 * The "ssh" subsection key.
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_SSH_SUBSECTION = "ssh";
+
+	/**
+	 * The "defaultKeyCommand" key.
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_SSH_DEFAULT_KEY_COMMAND = "defaultKeyCommand";
+
+	/**
+	 * The "allowedSignersFile" key.
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_SSH_ALLOWED_SIGNERS_FILE = "allowedSignersFile";
+
+	/**
+	 * The "revocationFile" key,
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_SSH_REVOCATION_FILE = "revocationFile";
+
+	/**
 	 * The "commit" section
+	 *
 	 * @since 5.2
 	 */
 	public static final String CONFIG_COMMIT_SECTION = "commit";
@@ -1027,4 +1056,11 @@ public final class ConfigConstants {
 	 * @since 7.0
 	 */
 	public static final String CONFIG_KEY_USE_OBJECT_SIZE_INDEX = "useObjectSizeIndex";
+
+	/**
+	 * The "loadRevIndexInParallel" key
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL = "loadRevIndexInParallel";
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
index fb5c904..76ed36a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
@@ -61,6 +61,12 @@ public String toConfigValue() {
 
 	private final boolean forceAnnotated;
 
+	private final String sshDefaultKeyCommand;
+
+	private final String sshAllowedSignersFile;
+
+	private final String sshRevocationFile;
+
 	/**
 	 * Create a new GPG config that reads the configuration from config.
 	 *
@@ -88,6 +94,17 @@ public GpgConfig(Config config) {
 				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
 		forceAnnotated = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
 				ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false);
+		sshDefaultKeyCommand = config.getString(
+				ConfigConstants.CONFIG_GPG_SECTION,
+				ConfigConstants.CONFIG_SSH_SUBSECTION,
+				ConfigConstants.CONFIG_KEY_SSH_DEFAULT_KEY_COMMAND);
+		sshAllowedSignersFile = config.getString(
+				ConfigConstants.CONFIG_GPG_SECTION,
+				ConfigConstants.CONFIG_SSH_SUBSECTION,
+				ConfigConstants.CONFIG_KEY_SSH_ALLOWED_SIGNERS_FILE);
+		sshRevocationFile = config.getString(ConfigConstants.CONFIG_GPG_SECTION,
+				ConfigConstants.CONFIG_SSH_SUBSECTION,
+				ConfigConstants.CONFIG_KEY_SSH_REVOCATION_FILE);
 	}
 
 	/**
@@ -151,4 +168,37 @@ public boolean isSignAllTags() {
 	public boolean isSignAnnotated() {
 		return forceAnnotated;
 	}
+
+	/**
+	 * Retrieves the value of git config {@code gpg.ssh.defaultKeyCommand}.
+	 *
+	 * @return the value of {@code gpg.ssh.defaultKeyCommand}
+	 *
+	 * @since 7.1
+	 */
+	public String getSshDefaultKeyCommand() {
+		return sshDefaultKeyCommand;
+	}
+
+	/**
+	 * Retrieves the value of git config {@code gpg.ssh.allowedSignersFile}.
+	 *
+	 * @return the value of {@code gpg.ssh.allowedSignersFile}
+	 *
+	 * @since 7.1
+	 */
+	public String getSshAllowedSignersFile() {
+		return sshAllowedSignersFile;
+	}
+
+	/**
+	 * Retrieves the value of git config {@code gpg.ssh.revocationFile}.
+	 *
+	 * @return the value of {@code gpg.ssh.revocationFile}
+	 *
+	 * @since 7.1
+	 */
+	public String getSshRevocationFile() {
+		return sshRevocationFile;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
index 3ba055a..5d3db9e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
@@ -13,9 +13,11 @@
 package org.eclipse.jgit.lib;
 
 import java.io.Serializable;
-import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.Date;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -49,6 +51,17 @@ public static TimeZone getTimeZone(int tzOffset) {
 	}
 
 	/**
+	 * Translate a minutes offset into a ZoneId
+	 *
+	 * @param tzOffset as minutes east of UTC
+	 * @return a ZoneId  for this offset
+	 * @since 7.1
+	 */
+	public static ZoneId getZoneId(int tzOffset) {
+		return ZoneOffset.ofHoursMinutes(tzOffset / 60, tzOffset % 60);
+	}
+
+	/**
 	 * Format a timezone offset.
 	 *
 	 * @param r
@@ -121,13 +134,17 @@ public static void appendSanitized(StringBuilder r, String str) {
 		}
 	}
 
+	// Write offsets as [+-]HHMM
+	private static final DateTimeFormatter OFFSET_FORMATTER = DateTimeFormatter
+			.ofPattern("Z", Locale.US); //$NON-NLS-1$
+
 	private final String name;
 
 	private final String emailAddress;
 
-	private final long when;
+	private final Instant when;
 
-	private final int tzOffset;
+	private final ZoneId tzOffset;
 
 	/**
 	 * Creates new PersonIdent from config info in repository, with current time.
@@ -160,7 +177,7 @@ public PersonIdent(PersonIdent pi) {
 	 *            a {@link java.lang.String} object.
 	 */
 	public PersonIdent(String aName, String aEmailAddress) {
-		this(aName, aEmailAddress, SystemReader.getInstance().getCurrentTime());
+		this(aName, aEmailAddress, SystemReader.getInstance().now());
 	}
 
 	/**
@@ -177,7 +194,7 @@ public PersonIdent(String aName, String aEmailAddress) {
 	 */
 	public PersonIdent(String aName, String aEmailAddress,
 			ProposedTimestamp when) {
-		this(aName, aEmailAddress, when.millis());
+		this(aName, aEmailAddress, when.instant());
 	}
 
 	/**
@@ -189,8 +206,25 @@ public PersonIdent(String aName, String aEmailAddress,
 	 *            local time
 	 * @param tz
 	 *            time zone
+	 * @deprecated Use {@link #PersonIdent(PersonIdent, Instant, ZoneId)} instead.
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(PersonIdent pi, Date when, TimeZone tz) {
+		this(pi.getName(), pi.getEmailAddress(), when.toInstant(), tz.toZoneId());
+	}
+
+	/**
+	 * Copy a PersonIdent, but alter the clone's time stamp
+	 *
+	 * @param pi
+	 *            original {@link org.eclipse.jgit.lib.PersonIdent}
+	 * @param when
+	 *            local time
+	 * @param tz
+	 *            time zone offset
+	 * @since 7.1
+	 */
+	public PersonIdent(PersonIdent pi, Instant when, ZoneId tz) {
 		this(pi.getName(), pi.getEmailAddress(), when, tz);
 	}
 
@@ -202,9 +236,12 @@ public PersonIdent(PersonIdent pi, Date when, TimeZone tz) {
 	 *            original {@link org.eclipse.jgit.lib.PersonIdent}
 	 * @param aWhen
 	 *            local time
+	 * @deprecated Use the variant with an Instant instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(PersonIdent pi, Date aWhen) {
-		this(pi.getName(), pi.getEmailAddress(), aWhen.getTime(), pi.tzOffset);
+		this(pi.getName(), pi.getEmailAddress(), aWhen.toInstant(),
+				pi.tzOffset);
 	}
 
 	/**
@@ -218,7 +255,7 @@ public PersonIdent(PersonIdent pi, Date aWhen) {
 	 * @since 6.1
 	 */
 	public PersonIdent(PersonIdent pi, Instant aWhen) {
-		this(pi.getName(), pi.getEmailAddress(), aWhen.toEpochMilli(), pi.tzOffset);
+		this(pi.getName(), pi.getEmailAddress(), aWhen, pi.tzOffset);
 	}
 
 	/**
@@ -230,11 +267,12 @@ public PersonIdent(PersonIdent pi, Instant aWhen) {
 	 *            local time stamp
 	 * @param aTZ
 	 *            time zone
+	 * @deprecated Use the variant with Instant and ZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(final String aName, final String aEmailAddress,
 			final Date aWhen, final TimeZone aTZ) {
-		this(aName, aEmailAddress, aWhen.getTime(), aTZ.getOffset(aWhen
-				.getTime()) / (60 * 1000));
+		this(aName, aEmailAddress, aWhen.toInstant(), aTZ.toZoneId());
 	}
 
 	/**
@@ -252,10 +290,16 @@ public PersonIdent(final String aName, final String aEmailAddress,
 	 */
 	public PersonIdent(final String aName, String aEmailAddress, Instant aWhen,
 			ZoneId zoneId) {
-		this(aName, aEmailAddress, aWhen.toEpochMilli(),
-				TimeZone.getTimeZone(zoneId)
-						.getOffset(aWhen
-				.toEpochMilli()) / (60 * 1000));
+		if (aName == null)
+			throw new IllegalArgumentException(
+					JGitText.get().personIdentNameNonNull);
+		if (aEmailAddress == null)
+			throw new IllegalArgumentException(
+					JGitText.get().personIdentEmailNonNull);
+		name = aName;
+		emailAddress = aEmailAddress;
+		when = aWhen;
+		tzOffset = zoneId;
 	}
 
 	/**
@@ -267,15 +311,18 @@ public PersonIdent(final String aName, String aEmailAddress, Instant aWhen,
 	 *            local time stamp
 	 * @param aTZ
 	 *            time zone
+	 * @deprecated Use the variant with Instant and ZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(PersonIdent pi, long aWhen, int aTZ) {
-		this(pi.getName(), pi.getEmailAddress(), aWhen, aTZ);
+		this(pi.getName(), pi.getEmailAddress(), Instant.ofEpochMilli(aWhen),
+				getZoneId(aTZ));
 	}
 
 	private PersonIdent(final String aName, final String aEmailAddress,
-			long when) {
+			Instant when) {
 		this(aName, aEmailAddress, when, SystemReader.getInstance()
-				.getTimezone(when));
+				.getTimeZoneAt(when));
 	}
 
 	private PersonIdent(UserConfig config) {
@@ -298,19 +345,12 @@ private PersonIdent(UserConfig config) {
 	 *            local time stamp
 	 * @param aTZ
 	 *            time zone
+	 * @deprecated Use  the variant with Instant and ZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(final String aName, final String aEmailAddress,
 			final long aWhen, final int aTZ) {
-		if (aName == null)
-			throw new IllegalArgumentException(
-					JGitText.get().personIdentNameNonNull);
-		if (aEmailAddress == null)
-			throw new IllegalArgumentException(
-					JGitText.get().personIdentEmailNonNull);
-		name = aName;
-		emailAddress = aEmailAddress;
-		when = aWhen;
-		tzOffset = aTZ;
+		this(aName, aEmailAddress, Instant.ofEpochMilli(aWhen), getZoneId(aTZ));
 	}
 
 	/**
@@ -335,9 +375,12 @@ public String getEmailAddress() {
 	 * Get timestamp
 	 *
 	 * @return timestamp
+	 *
+	 * @deprecated Use getWhenAsInstant instead
 	 */
+	@Deprecated(since = "7.1")
 	public Date getWhen() {
-		return new Date(when);
+		return Date.from(when);
 	}
 
 	/**
@@ -347,16 +390,19 @@ public Date getWhen() {
 	 * @since 6.1
 	 */
 	public Instant getWhenAsInstant() {
-		return Instant.ofEpochMilli(when);
+		return when;
 	}
 
 	/**
 	 * Get this person's declared time zone
 	 *
 	 * @return this person's declared time zone; null if time zone is unknown.
+	 *
+	 * @deprecated Use getZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public TimeZone getTimeZone() {
-		return getTimeZone(tzOffset);
+		return TimeZone.getTimeZone(tzOffset);
 	}
 
 	/**
@@ -366,7 +412,17 @@ public TimeZone getTimeZone() {
 	 * @since 6.1
 	 */
 	public ZoneId getZoneId() {
-		return getTimeZone().toZoneId();
+		return tzOffset;
+	}
+
+	/**
+	 * Return the offset in this timezone at the specific time
+	 *
+	 * @return the offset
+	 * @since 7.1
+	 */
+	public ZoneOffset getZoneOffset() {
+		return tzOffset.getRules().getOffset(when);
 	}
 
 	/**
@@ -374,9 +430,11 @@ public ZoneId getZoneId() {
 	 *
 	 * @return this person's declared time zone as minutes east of UTC. If the
 	 *         timezone is to the west of UTC it is negative.
+	 * @deprecated Use {@link #getZoneOffset()} and read minutes from there
 	 */
+	@Deprecated(since = "7.1")
 	public int getTimeZoneOffset() {
-		return tzOffset;
+		return getZoneOffset().getTotalSeconds() / 60;
 	}
 
 	/**
@@ -388,7 +446,7 @@ public int getTimeZoneOffset() {
 	public int hashCode() {
 		int hc = getEmailAddress().hashCode();
 		hc *= 31;
-		hc += (int) (when / 1000L);
+		hc += when.hashCode();
 		return hc;
 	}
 
@@ -398,7 +456,9 @@ public boolean equals(Object o) {
 			final PersonIdent p = (PersonIdent) o;
 			return getName().equals(p.getName())
 					&& getEmailAddress().equals(p.getEmailAddress())
-					&& when / 1000L == p.when / 1000L;
+					// commmit timestamps are stored with 1 second precision
+					&& when.truncatedTo(ChronoUnit.SECONDS)
+							.equals(p.when.truncatedTo(ChronoUnit.SECONDS));
 		}
 		return false;
 	}
@@ -414,9 +474,9 @@ public String toExternalString() {
 		r.append(" <"); //$NON-NLS-1$
 		appendSanitized(r, getEmailAddress());
 		r.append("> "); //$NON-NLS-1$
-		r.append(when / 1000);
+		r.append(when.toEpochMilli() / 1000);
 		r.append(' ');
-		appendTimezone(r, tzOffset);
+		r.append(OFFSET_FORMATTER.format(getZoneOffset()));
 		return r.toString();
 	}
 
@@ -424,19 +484,16 @@ public String toExternalString() {
 	@SuppressWarnings("nls")
 	public String toString() {
 		final StringBuilder r = new StringBuilder();
-		final SimpleDateFormat dtfmt;
-		dtfmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US);
-		dtfmt.setTimeZone(getTimeZone());
-
+		DateTimeFormatter dtfmt = DateTimeFormatter
+				.ofPattern("EEE MMM d HH:mm:ss yyyy Z", Locale.US) //$NON-NLS-1$
+				.withZone(tzOffset);
 		r.append("PersonIdent[");
 		r.append(getName());
 		r.append(", ");
 		r.append(getEmailAddress());
 		r.append(", ");
-		r.append(dtfmt.format(Long.valueOf(when)));
+		r.append(dtfmt.format(when));
 		r.append("]");
-
 		return r.toString();
 	}
 }
-
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
index 2cf2418..09cb5a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -26,6 +26,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.PackRefsCommand;
 
 /**
  * Abstraction of name to {@link org.eclipse.jgit.lib.ObjectId} mapping.
@@ -160,7 +161,7 @@ public Collection<String> getConflictingNames(String name)
 			if (existing.startsWith(prefix))
 				conflicting.add(existing);
 
-		return conflicting;
+		return Collections.unmodifiableList(conflicting);
 	}
 
 	/**
@@ -593,4 +594,22 @@ public static Ref findRef(Map<String, Ref> map, String name) {
 		}
 		return null;
 	}
+
+	/**
+	 * Optimize pack ref storage.
+	 *
+	 * @param pm
+	 *            a progress monitor
+	 *
+	 * @param packRefs
+	 *            {@link PackRefsCommand} to control ref packing behavior
+	 *
+	 * @throws java.io.IOException
+	 *             if an IO error occurred
+	 * @since 7.1
+	 */
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		// nothing
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
index 1162a61..fc5ab62 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
@@ -22,6 +22,7 @@
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -185,12 +186,15 @@ protected RevCommit getBaseCommit(RevCommit a, RevCommit b, int callDepth)
 				if (mergeTrees(bcTree, currentBase.getTree(),
 						nextBase.getTree(), true))
 					currentBase = createCommitForTree(resultTree, parents);
-				else
+				else {
+					String failedPaths = failingPathsMessage();
 					throw new NoMergeBaseException(
 							NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION,
 							MessageFormat.format(
 									JGitText.get().mergeRecursiveConflictsWhenMergingCommonAncestors,
-									currentBase.getName(), nextBase.getName()));
+									currentBase.getName(), nextBase.getName(),
+									failedPaths));
+				}
 			}
 		} finally {
 			inCore = oldIncore;
@@ -236,4 +240,17 @@ private static PersonIdent mockAuthor(List<RevCommit> parents) {
 				new Date((time + 1) * 1000L),
 				TimeZone.getTimeZone("GMT+0000")); //$NON-NLS-1$
 	}
+
+	private String failingPathsMessage() {
+		int max = 25;
+		String failedPaths = failingPaths.entrySet().stream().limit(max)
+				.map(entry -> entry.getKey() + ":" + entry.getValue()) //$NON-NLS-1$
+				.collect(Collectors.joining("\n")); //$NON-NLS-1$
+
+		if (failingPaths.size() > max) {
+			failedPaths = String.format("%s\n... (%s failing paths omitted)", //$NON-NLS-1$
+					failedPaths, Integer.valueOf(failingPaths.size() - max));
+		}
+		return failedPaths;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index a50a644..dc96f65 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -1281,6 +1281,13 @@ protected boolean processEntry(CanonicalTreeParser base,
 					default:
 						break;
 				}
+				if (ignoreConflicts) {
+					// If the path is selected to be treated as binary via attributes, we do not perform
+					// content merge. When ignoreConflicts = true, we simply keep OURS to allow virtual commit
+					// to be built.
+					keep(ourDce);
+					return true;
+				}
 				// add the conflicting path to merge result
 				String currentPath = tw.getPathString();
 				MergeResult<RawText> result = new MergeResult<>(
@@ -1320,8 +1327,12 @@ protected boolean processEntry(CanonicalTreeParser base,
 					addToCheckout(currentPath, null, attributes);
 					return true;
 				} catch (BinaryBlobException e) {
-					// if the file is binary in either OURS, THEIRS or BASE
-					// here, we don't have an option to ignore conflicts
+					// The file is binary in either OURS, THEIRS or BASE
+					if (ignoreConflicts) {
+						// When ignoreConflicts = true, we simply keep OURS to allow virtual commit to be built.
+						keep(ourDce);
+						return true;
+					}
 				}
 			}
 			switch (getContentMergeStrategy()) {
@@ -1362,6 +1373,8 @@ protected boolean processEntry(CanonicalTreeParser base,
 					}
 				}
 			} else {
+				// This is reachable if contentMerge() call above threw BinaryBlobException, so we don't
+				// need to check ignoreConflicts here, since it's already handled above.
 				result.setContainsConflicts(true);
 				addConflict(base, ours, theirs);
 				unmergedPaths.add(currentPath);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
index 8373d68..863b794 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
@@ -50,7 +50,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.zip.Deflater;
 
-import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 
@@ -995,7 +995,7 @@ public void setExecutor(Executor executor) {
 	 *
 	 * @return the index version, the special version 0 designates the oldest
 	 *         (most compatible) format available for the objects.
-	 * @see PackIndexWriter
+	 * @see BasePackIndexWriter
 	 */
 	public int getIndexVersion() {
 		return indexVersion;
@@ -1009,7 +1009,7 @@ public int getIndexVersion() {
 	 * @param version
 	 *            the version to write. The special version 0 designates the
 	 *            oldest (most compatible) format available for the objects.
-	 * @see PackIndexWriter
+	 * @see BasePackIndexWriter
 	 */
 	public void setIndexVersion(int version) {
 		indexVersion = version;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
index a0194ea..8120df0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
@@ -11,8 +11,6 @@
 
 package org.eclipse.jgit.transport;
 
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.Iterator;
 import java.util.ServiceLoader;
 
@@ -99,9 +97,8 @@ public static void setInstance(SshSessionFactory newFactory) {
 	 * @since 5.2
 	 */
 	public static String getLocalUserName() {
-		return AccessController
-				.doPrivileged((PrivilegedAction<String>) () -> SystemReader
-						.getInstance().getProperty(Constants.OS_USER_NAME_KEY));
+		return SystemReader.getInstance()
+				.getProperty(Constants.OS_USER_NAME_KEY);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
index 5ba8270..d972067 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -2384,7 +2384,8 @@ else if (ref.getName().startsWith(Constants.R_HEADS))
 						: req.getDepth() - 1;
 				pw.setShallowPack(req.getDepth(), unshallowCommits);
 
-				// Ownership is transferred below
+				// dw borrows the reader from walk which is closed by #close
+				@SuppressWarnings("resource")
 				DepthWalk.RevWalk dw = new DepthWalk.RevWalk(
 						walk.getObjectReader(), walkDepth);
 				dw.setDeepenSince(req.getDeepenSince());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
index 125ee6c..95b8221 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
@@ -36,29 +36,39 @@
  */
 public interface HttpConnection {
 	/**
+	 * HttpURLConnection#HTTP_OK
+	 *
 	 * @see HttpURLConnection#HTTP_OK
 	 */
 	int HTTP_OK = java.net.HttpURLConnection.HTTP_OK;
 
 	/**
+	 * HttpURLConnection#HTTP_NOT_AUTHORITATIVE
+	 *
 	 * @see HttpURLConnection#HTTP_NOT_AUTHORITATIVE
 	 * @since 5.8
 	 */
 	int HTTP_NOT_AUTHORITATIVE = java.net.HttpURLConnection.HTTP_NOT_AUTHORITATIVE;
 
 	/**
+	 * HttpURLConnection#HTTP_MOVED_PERM
+	 *
 	 * @see HttpURLConnection#HTTP_MOVED_PERM
 	 * @since 4.7
 	 */
 	int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM;
 
 	/**
+	 * HttpURLConnection#HTTP_MOVED_TEMP
+	 *
 	 * @see HttpURLConnection#HTTP_MOVED_TEMP
 	 * @since 4.9
 	 */
 	int HTTP_MOVED_TEMP = java.net.HttpURLConnection.HTTP_MOVED_TEMP;
 
 	/**
+	 * HttpURLConnection#HTTP_SEE_OTHER
+	 *
 	 * @see HttpURLConnection#HTTP_SEE_OTHER
 	 * @since 4.9
 	 */
@@ -85,16 +95,22 @@ public interface HttpConnection {
 	int HTTP_11_MOVED_PERM = 308;
 
 	/**
+	 * HttpURLConnection#HTTP_NOT_FOUND
+	 *
 	 * @see HttpURLConnection#HTTP_NOT_FOUND
 	 */
 	int HTTP_NOT_FOUND = java.net.HttpURLConnection.HTTP_NOT_FOUND;
 
 	/**
+	 * HttpURLConnection#HTTP_UNAUTHORIZED
+	 *
 	 * @see HttpURLConnection#HTTP_UNAUTHORIZED
 	 */
 	int HTTP_UNAUTHORIZED = java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
 
 	/**
+	 * HttpURLConnection#HTTP_FORBIDDEN
+	 *
 	 * @see HttpURLConnection#HTTP_FORBIDDEN
 	 */
 	int HTTP_FORBIDDEN = java.net.HttpURLConnection.HTTP_FORBIDDEN;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java
index bcf79a2..33db6ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java
@@ -13,12 +13,12 @@
 
 package org.eclipse.jgit.treewalk.filter;
 
-import org.eclipse.jgit.util.RawParseUtils;
-
 import java.util.Arrays;
 import java.util.Set;
 import java.util.stream.Collectors;
 
+import org.eclipse.jgit.util.RawParseUtils;
+
 /**
  * Specialized set for byte arrays, interpreted as strings for use in
  * {@link PathFilterGroup.Group}. Most methods assume the hash is already know
@@ -141,13 +141,19 @@ boolean contains(byte[] toFind, int length, int hash) {
 	}
 
 	/**
+	 * Returns number of arrays in the set
+	 *
 	 * @return number of arrays in the set
 	 */
 	int size() {
 		return size;
 	}
 
-	/** @return true if {@link #size()} is 0. */
+	/**
+	 * Returns true if {@link #size()} is 0
+	 *
+	 * @return true if {@link #size()} is 0
+	 */
 	boolean isEmpty() {
 		return size == 0;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
index 860c1c9..59bbacf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -30,7 +30,6 @@
 import java.nio.file.Path;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileTime;
-import java.security.AccessControlException;
 import java.text.MessageFormat;
 import java.time.Duration;
 import java.time.Instant;
@@ -262,31 +261,6 @@ public static final class FileStoreAttributes {
 		private static final AtomicInteger threadNumber = new AtomicInteger(1);
 
 		/**
-		 * Don't use the default thread factory of the ForkJoinPool for the
-		 * CompletableFuture; it runs without any privileges, which causes
-		 * trouble if a SecurityManager is present.
-		 * <p>
-		 * Instead use normal daemon threads. They'll belong to the
-		 * SecurityManager's thread group, or use the one of the calling thread,
-		 * as appropriate.
-		 * </p>
-		 *
-		 * @see java.util.concurrent.Executors#newCachedThreadPool()
-		 */
-		private static final ExecutorService FUTURE_RUNNER = new ThreadPoolExecutor(
-				5, 5, 30L, TimeUnit.SECONDS,
-				new LinkedBlockingQueue<>(),
-				runnable -> {
-					Thread t = new Thread(runnable,
-							"JGit-FileStoreAttributeReader-" //$NON-NLS-1$
-							+ threadNumber.getAndIncrement());
-					// Make sure these threads don't prevent application/JVM
-					// shutdown.
-					t.setDaemon(true);
-					return t;
-				});
-
-		/**
 		 * Use a separate executor with at most one thread to synchronize
 		 * writing to the config. We write asynchronously since the config
 		 * itself might be on a different file system, which might otherwise
@@ -463,7 +437,7 @@ private static FileStoreAttributes getFileStoreAttributes(Path dir) {
 								locks.remove(s);
 							}
 							return attributes;
-						}, FUTURE_RUNNER);
+						});
 				f = f.exceptionally(e -> {
 					LOG.error(e.getLocalizedMessage(), e);
 					return Optional.empty();
@@ -1391,13 +1365,6 @@ protected static String readPipe(File dir, String[] command,
 			}
 		} catch (IOException e) {
 			LOG.error("Caught exception in FS.readPipe()", e); //$NON-NLS-1$
-		} catch (AccessControlException e) {
-			LOG.warn(MessageFormat.format(
-					JGitText.get().readPipeIsNotAllowedRequiredPermission,
-					command, dir, e.getPermission()));
-		} catch (SecurityException e) {
-			LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed,
-					command, dir));
 		}
 		if (debug) {
 			LOG.debug("readpipe returns null"); //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
index 635351a..2378791 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
@@ -14,8 +14,6 @@
 
 import java.io.File;
 import java.io.OutputStream;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -43,10 +41,7 @@ public class FS_Win32_Cygwin extends FS_Win32 {
 	 * @return true if cygwin is found
 	 */
 	public static boolean isCygwin() {
-		final String path = AccessController
-				.doPrivileged((PrivilegedAction<String>) () -> System
-						.getProperty("java.library.path") //$NON-NLS-1$
-				);
+		final String path = System.getProperty("java.library.path"); //$NON-NLS-1$
 		if (path == null)
 			return false;
 		File found = FS.searchPath(path, "cygpath.exe"); //$NON-NLS-1$
@@ -99,9 +94,7 @@ public File resolve(File dir, String pn) {
 
 	@Override
 	protected File userHomeImpl() {
-		final String home = AccessController.doPrivileged(
-				(PrivilegedAction<String>) () -> System.getenv("HOME") //$NON-NLS-1$
-		);
+		final String home = System.getenv("HOME"); //$NON-NLS-1$
 		if (home == null || home.length() == 0)
 			return super.userHomeImpl();
 		return resolve(new File("."), home); //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
index 6a4b396..f080056 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
@@ -28,7 +28,10 @@
  * used. One example is the parsing of the config parameter gc.pruneexpire. The
  * parser can handle only subset of what native gits approxidate parser
  * understands.
+ *
+ * @deprecated Use {@link GitTimeParser} instead.
  */
+@Deprecated(since = "7.1")
 public class GitDateParser {
 	/**
 	 * The Date representing never. Though this is a concrete value, most
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java
new file mode 100644
index 0000000..7d00fcd
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2024 Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.EnumMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Parses strings with time and date specifications into
+ * {@link java.time.Instant}.
+ *
+ * When git needs to parse strings specified by the user this parser can be
+ * used. One example is the parsing of the config parameter gc.pruneexpire. The
+ * parser can handle only subset of what native gits approxidate parser
+ * understands.
+ *
+ * @since 7.1
+ */
+public class GitTimeParser {
+
+	private static final Map<ParseableSimpleDateFormat, DateTimeFormatter> formatCache = new EnumMap<>(
+			ParseableSimpleDateFormat.class);
+
+	// An enum of all those formats which this parser can parse with the help of
+	// a DateTimeFormatter. There are other formats (e.g. the relative formats
+	// like "yesterday" or "1 week ago") which this parser can parse but which
+	// are not listed here because they are parsed without the help of a
+	// DateTimeFormatter.
+	enum ParseableSimpleDateFormat {
+		ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$
+		RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$
+		SHORT("yyyy-MM-dd"), // //$NON-NLS-1$
+		SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$
+		SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$
+		SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$
+		DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$
+		LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$
+
+		private final String formatStr;
+
+		ParseableSimpleDateFormat(String formatStr) {
+			this.formatStr = formatStr;
+		}
+	}
+
+	private GitTimeParser() {
+		// This class is not supposed to be instantiated
+	}
+
+	/**
+	 * Parses a string into a {@link java.time.LocalDateTime} using the default
+	 * locale. Since this parser also supports relative formats (e.g.
+	 * "yesterday") the caller can specify the reference date. These types of
+	 * strings can be parsed:
+	 * <ul>
+	 * <li>"never"</li>
+	 * <li>"now"</li>
+	 * <li>"yesterday"</li>
+	 * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
+	 * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
+	 * ' one can use '.' to separate the words</li>
+	 * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
+	 * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
+	 * <li>"yyyy-MM-dd"</li>
+	 * <li>"yyyy.MM.dd"</li>
+	 * <li>"MM/dd/yyyy",</li>
+	 * <li>"dd.MM.yyyy"</li>
+	 * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
+	 * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
+	 * </ul>
+	 *
+	 * @param dateStr
+	 *            the string to be parsed
+	 * @return the parsed {@link java.time.LocalDateTime}
+	 * @throws java.text.ParseException
+	 *             if the given dateStr was not recognized
+	 */
+	public static LocalDateTime parse(String dateStr) throws ParseException {
+		return parse(dateStr, SystemReader.getInstance().civilNow());
+	}
+
+	// Only tests seem to use this method
+	static LocalDateTime parse(String dateStr, LocalDateTime now)
+			throws ParseException {
+		dateStr = dateStr.trim();
+
+		if (dateStr.equalsIgnoreCase("never")) { //$NON-NLS-1$
+			return LocalDateTime.MAX;
+		}
+		LocalDateTime ret = parseRelative(dateStr, now);
+		if (ret != null) {
+			return ret;
+		}
+		for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) {
+			try {
+				return parseSimple(dateStr, f);
+			} catch (DateTimeParseException e) {
+				// simply proceed with the next parser
+			}
+		}
+		ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values();
+		StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$
+				.append(values[0].formatStr);
+		for (int i = 1; i < values.length; i++) {
+			allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$
+		}
+		allFormats.append("\""); //$NON-NLS-1$
+		throw new ParseException(
+				MessageFormat.format(JGitText.get().cannotParseDate, dateStr,
+						allFormats.toString()),
+				0);
+	}
+
+	// tries to parse a string with the formats supported by DateTimeFormatter
+	private static LocalDateTime parseSimple(String dateStr,
+			ParseableSimpleDateFormat f) throws DateTimeParseException {
+		DateTimeFormatter dateFormat = formatCache.computeIfAbsent(f,
+				format -> DateTimeFormatter
+						.ofPattern(f.formatStr)
+						.withLocale(SystemReader.getInstance().getLocale()));
+		TemporalAccessor parsed = dateFormat.parse(dateStr);
+		return parsed.isSupported(ChronoField.HOUR_OF_DAY)
+				? LocalDateTime.from(parsed)
+				: LocalDate.from(parsed).atStartOfDay();
+	}
+
+	// tries to parse a string with a relative time specification
+	@SuppressWarnings("nls")
+	@Nullable
+	private static LocalDateTime parseRelative(String dateStr,
+			LocalDateTime now) {
+		// check for the static words "yesterday" or "now"
+		if (dateStr.equals("now")) {
+			return now;
+		}
+
+		if (dateStr.equals("yesterday")) {
+			return now.minusDays(1);
+		}
+
+		// parse constructs like "3 days ago", "5.week.2.day.ago"
+		String[] parts = dateStr.split("\\.| ", -1);
+		int partsLength = parts.length;
+		// check we have an odd number of parts (at least 3) and that the last
+		// part is "ago"
+		if (partsLength < 3 || (partsLength & 1) == 0
+				|| !parts[parts.length - 1].equals("ago")) {
+			return null;
+		}
+		int number;
+		for (int i = 0; i < parts.length - 2; i += 2) {
+			try {
+				number = Integer.parseInt(parts[i]);
+			} catch (NumberFormatException e) {
+				return null;
+			}
+			if (parts[i + 1] == null) {
+				return null;
+			}
+			switch (parts[i + 1]) {
+			case "year":
+			case "years":
+				now = now.minusYears(number);
+				break;
+			case "month":
+			case "months":
+				now = now.minusMonths(number);
+				break;
+			case "week":
+			case "weeks":
+				now = now.minusWeeks(number);
+				break;
+			case "day":
+			case "days":
+				now = now.minusDays(number);
+				break;
+			case "hour":
+			case "hours":
+				now = now.minusHours(number);
+				break;
+			case "minute":
+			case "minutes":
+				now = now.minusMinutes(number);
+				break;
+			case "second":
+			case "seconds":
+				now = now.minusSeconds(number);
+				break;
+			default:
+				return null;
+			}
+		}
+		return now;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
index d957deb..efa6e7dd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
@@ -43,14 +43,18 @@ public void add(double x) {
 	}
 
 	/**
-	 * @return number of the added values
+	 * Returns the number of added values
+	 *
+	 * @return  the number of added values
 	 */
 	public int count() {
 		return n;
 	}
 
 	/**
-	 * @return minimum of the added values
+	 * Returns the smallest value added
+	 *
+	 * @return the smallest value added
 	 */
 	public double min() {
 		if (n < 1) {
@@ -60,7 +64,9 @@ public double min() {
 	}
 
 	/**
-	 * @return maximum of the added values
+	 * Returns the biggest value added
+	 *
+	 * @return the biggest value added
 	 */
 	public double max() {
 		if (n < 1) {
@@ -70,9 +76,10 @@ public double max() {
 	}
 
 	/**
-	 * @return average of the added values
+	 * Returns the average of the added values
+	 *
+	 * @return the average of the added values
 	 */
-
 	public double avg() {
 		if (n < 1) {
 			return Double.NaN;
@@ -81,7 +88,9 @@ public double avg() {
 	}
 
 	/**
-	 * @return variance of the added values
+	 * Returns the variance of the added values
+	 *
+	 * @return the variance of the added values
 	 */
 	public double var() {
 		if (n < 2) {
@@ -91,7 +100,9 @@ public double var() {
 	}
 
 	/**
-	 * @return standard deviation of the added values
+	 * Returns the standard deviation of the added values
+	 *
+	 * @return the standard deviation of the added values
 	 */
 	public double stddev() {
 		return Math.sqrt(this.var());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
index ed62c71..55cc878 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
@@ -23,10 +23,12 @@
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.Locale;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicReference;
@@ -169,6 +171,11 @@ public long getCurrentTime() {
 		}
 
 		@Override
+		public Instant now() {
+			return Instant.now();
+		}
+
+		@Override
 		public int getTimezone(long when) {
 			return getTimeZone().getOffset(when) / (60 * 1000);
 		}
@@ -230,9 +237,19 @@ public long getCurrentTime() {
 		}
 
 		@Override
+		public Instant now() {
+			return delegate.now();
+		}
+
+		@Override
 		public int getTimezone(long when) {
 			return delegate.getTimezone(when);
 		}
+
+		@Override
+		public ZoneOffset getTimeZoneAt(Instant when) {
+			return delegate.getTimeZoneAt(when);
+		}
 	}
 
 	private static volatile SystemReader INSTANCE = DEFAULT;
@@ -503,10 +520,37 @@ private void updateAll(Config config)
 	 * Get the current system time
 	 *
 	 * @return the current system time
+	 *
+	 * @deprecated Use {@link #now()}
 	 */
+	@Deprecated
 	public abstract long getCurrentTime();
 
 	/**
+	 * Get the current system time
+	 *
+	 * @return the current system time
+	 *
+	 * @since 7.1
+	 */
+	public Instant now() {
+		// Subclasses overriding getCurrentTime should keep working
+		// TODO(ifrade): Once we remove getCurrentTime, use Instant.now()
+		return Instant.ofEpochMilli(getCurrentTime());
+	}
+
+	/**
+	 * Get "now" as civil time, in the System timezone
+	 *
+	 * @return the current system time
+	 *
+	 * @since 7.1
+	 */
+	public LocalDateTime civilNow() {
+		return LocalDateTime.ofInstant(now(), getTimeZoneId());
+	}
+
+	/**
 	 * Get clock instance preferred by this system.
 	 *
 	 * @return clock instance preferred by this system.
@@ -522,20 +566,48 @@ public MonotonicClock getClock() {
 	 * @param when
 	 *            a system timestamp
 	 * @return the local time zone
+	 *
+	 * @deprecated Use {@link #getTimeZoneAt(Instant)} instead.
 	 */
+	@Deprecated
 	public abstract int getTimezone(long when);
 
 	/**
+	 * Get the local time zone offset at "when" time
+	 *
+	 * @param when
+	 *            a system timestamp
+	 * @return the local time zone
+	 * @since 7.1
+	 */
+	public ZoneOffset getTimeZoneAt(Instant when) {
+		return getTimeZoneId().getRules().getOffset(when);
+	}
+
+	/**
 	 * Get system time zone, possibly mocked for testing
 	 *
 	 * @return system time zone, possibly mocked for testing
 	 * @since 1.2
+	 *
+	 * @deprecated Use {@link #getTimeZoneId()}
 	 */
+	@Deprecated
 	public TimeZone getTimeZone() {
 		return TimeZone.getDefault();
 	}
 
 	/**
+	 * Get system time zone, possibly mocked for testing
+	 *
+	 * @return system time zone, possibly mocked for testing
+	 * @since 7.1
+	 */
+	public ZoneId getTimeZoneId() {
+		return ZoneId.systemDefault();
+	}
+
+	/**
 	 * Get the locale to use
 	 *
 	 * @return the locale to use
@@ -670,9 +742,7 @@ public boolean isPerformanceTraceEnabled() {
 	}
 
 	private String getOsName() {
-		return AccessController.doPrivileged(
-				(PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$
-		);
+		return getProperty("os.name"); //$NON-NLS-1$
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java
index 4764676..13982b1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java
@@ -11,8 +11,6 @@
 
 import java.io.IOException;
 import java.io.Writer;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 
 import org.eclipse.jgit.util.SystemReader;
 
@@ -35,10 +33,7 @@ public class ThrowingPrintWriter extends Writer {
 	 */
 	public ThrowingPrintWriter(Writer out) {
 		this.out = out;
-		LF = AccessController
-				.doPrivileged((PrivilegedAction<String>) () -> SystemReader
-						.getInstance().getProperty("line.separator") //$NON-NLS-1$
-				);
+		LF = SystemReader.getInstance().getProperty("line.separator"); //$NON-NLS-1$
 	}
 
 	@Override
diff --git a/pom.xml b/pom.xml
index ef6018b..9a0770c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>org.eclipse.jgit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>7.0.2-SNAPSHOT</version>
+  <version>7.1.2-SNAPSHOT</version>
 
   <name>JGit - Parent</name>
   <url>${jgit-url}</url>
@@ -118,8 +118,8 @@
 
     <project.build.outputTimestamp>${commit.time.iso}</project.build.outputTimestamp>
 
-    <jgit-last-release-version>6.10.0.202406032230-r</jgit-last-release-version>
-    <ant-version>1.10.14</ant-version>
+    <jgit-last-release-version>7.0.0.202409031743-r</jgit-last-release-version>
+    <ant-version>1.10.15</ant-version>
     <apache-sshd-version>2.14.0</apache-sshd-version>
     <jsch-version>0.1.55</jsch-version>
     <jzlib-version>1.1.3</jzlib-version>
@@ -130,14 +130,14 @@
     <commons-compress-version>1.27.1</commons-compress-version>
     <osgi-core-version>6.0.0</osgi-core-version>
     <servlet-api-version>6.1.0</servlet-api-version>
-    <jetty-version>12.0.12</jetty-version>
+    <jetty-version>12.0.15</jetty-version>
     <japicmp-version>0.21.2</japicmp-version>
     <httpclient-version>4.5.14</httpclient-version>
     <httpcore-version>4.4.16</httpcore-version>
     <slf4j-version>1.7.36</slf4j-version>
     <maven-javadoc-plugin-version>3.6.3</maven-javadoc-plugin-version>
     <gson-version>2.11.0</gson-version>
-    <bouncycastle-version>1.78.1</bouncycastle-version>
+    <bouncycastle-version>1.79</bouncycastle-version>
     <spotbugs-maven-plugin-version>4.8.5.0</spotbugs-maven-plugin-version>
     <maven-project-info-reports-plugin-version>3.5.1</maven-project-info-reports-plugin-version>
     <maven-jxr-plugin-version>3.3.2</maven-jxr-plugin-version>
@@ -147,8 +147,8 @@
     <plexus-compiler-version>2.13.0</plexus-compiler-version>
     <hamcrest-version>2.2</hamcrest-version>
     <assertj-version>3.26.3</assertj-version>
-    <jna-version>5.14.0</jna-version>
-    <byte-buddy-version>1.15.0</byte-buddy-version>
+    <jna-version>5.15.0</jna-version>
+    <byte-buddy-version>1.15.10</byte-buddy-version>
 
     <!-- Properties to enable jacoco code coverage analysis -->
     <sonar.core.codeCoveragePlugin>jacoco</sonar.core.codeCoveragePlugin>
@@ -911,7 +911,7 @@
       <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-io</artifactId>
-        <version>2.16.1</version>
+        <version>2.17.0</version>
       </dependency>
 
       <dependency>
@@ -1020,7 +1020,7 @@
       <dependency>
         <groupId>org.mockito</groupId>
         <artifactId>mockito-core</artifactId>
-        <version>5.12.0</version>
+        <version>5.14.2</version>
       </dependency>
 
       <dependency>
diff --git a/tools/BUILD b/tools/BUILD
index 8c424b3..844f004 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -10,6 +10,7 @@
     java_runtime = "@rules_java//toolchains:remotejdk_17",
     package_configuration = [
         ":error_prone",
+        ":error_prone_tests",
     ],
     source_version = "17",
     target_version = "17",
@@ -22,6 +23,7 @@
     java_runtime = "@rules_java//toolchains:remotejdk_21",
     package_configuration = [
         ":error_prone",
+        ":error_prone_tests",
     ],
     source_version = "21",
     target_version = "21",
@@ -32,9 +34,7 @@
 # enabled. This warnings list is originally based on:
 # https://github.com/bazelbuild/BUILD_file_generator/blob/master/tools/bazel_defs/java.bzl
 # However, feel free to add any additional errors. Thus far they have all been pretty useful.
-java_package_configuration(
-    name = "error_prone",
-    javacopts = [
+errorprone_checks = [
         "-XepDisableWarningsInGeneratedCode",
         # The XepDisableWarningsInGeneratedCode disables only warnings, but
         # not errors. We should manually exclude all files generated by
@@ -422,37 +422,57 @@
         "-Xep:WrongOneof:ERROR",
         "-Xep:XorPower:ERROR",
         "-Xep:ZoneIdOfZ:ERROR",
-    ],
+]
+
+
+exclude_in_tests = ["-Xep:EmptyBlockTag:WARN",
+                    "-Xep:MissingSummary:WARN"]
+
+java_package_configuration(
+    name = "error_prone",
+    javacopts = errorprone_checks,
     packages = ["error_prone_packages"],
 )
 
+java_package_configuration(
+    name = "error_prone_tests",
+    javacopts = [ check for check in errorprone_checks if check not in exclude_in_tests],
+    packages = ["error_prone_packages_test"],
+)
+
 package_group(
     name = "error_prone_packages",
     packages = [
-        "//org.eclipse.jgit.ant.test/...",
         "//org.eclipse.jgit.ant/...",
         "//org.eclipse.jgit.archive/...",
-        "//org.eclipse.jgit.gpg.bc.test/...",
         "//org.eclipse.jgit.gpg.bc/...",
         "//org.eclipse.jgit.http.apache/...",
         "//org.eclipse.jgit.http.server/...",
-        "//org.eclipse.jgit.http.test/...",
         "//org.eclipse.jgit.junit.ssh/...",
         "//org.eclipse.jgit.junit/...",
         "//org.eclipse.jgit.junit/http/...",
-        "//org.eclipse.jgit.lfs.server.test/...",
         "//org.eclipse.jgit.lfs.server/...",
-        "//org.eclipse.jgit.lfs.test/...",
         "//org.eclipse.jgit.lfs/...",
-        "//org.eclipse.jgit.pgm.test/...",
         "//org.eclipse.jgit.pgm/...",
         "//org.eclipse.jgit.ssh.apache.agent/...",
-        "//org.eclipse.jgit.ssh.apache.test/...",
         "//org.eclipse.jgit.ssh.apache/...",
-        "//org.eclipse.jgit.ssh.jsch.test/...",
         "//org.eclipse.jgit.ssh.jsch/...",
-        "//org.eclipse.jgit.test/...",
         "//org.eclipse.jgit.ui/...",
         "//org.eclipse.jgit/...",
     ],
 )
+
+package_group(
+    name = "error_prone_packages_test",
+    packages = [
+        "//org.eclipse.jgit.ant.test/...",
+        "//org.eclipse.jgit.gpg.bc.test/...",
+        "//org.eclipse.jgit.http.test/...",
+        "//org.eclipse.jgit.lfs.server.test/...",
+        "//org.eclipse.jgit.lfs.test/...",
+        "//org.eclipse.jgit.pgm.test/...",
+        "//org.eclipse.jgit.ssh.apache.test/...",
+        "//org.eclipse.jgit.ssh.jsch.test/...",
+        "//org.eclipse.jgit.test/...",
+    ],
+)