* [PATCH] http: add http.sslVerifyStatus to check stapled OCSP responses
@ 2026-08-11 17:02 graysongordon-gl
2026-08-11 19:28 ` Junio C Hamano
2026-08-11 20:44 ` [PATCH v2] " graysongordon-gl
0 siblings, 2 replies; 16+ messages in thread
From: graysongordon-gl @ 2026-08-11 17:02 UTC (permalink / raw)
To: git; +Cc: gitster, peff, avarab, ps, Grayson Gordon
From: Grayson Gordon <graysongordon1@gmail.com>
git asks libcurl to verify the peer certificate and the hostname, but it
never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
TLS extension is never requested and any stapled OCSP response the server
does send is ignored.
On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
response to the application and takes no view on it:
SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
whether the returned OCSP response(s) are acceptable or not", and libcurl
only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
will fetch from a server whose own staple says its certificate has been
revoked.
A GnuTLS-linked build behaves differently, and the difference does not
come from curl. GnuTLS consults a stapled response inside
gnutls_certificate_verify_peers(), so the failure surfaces through the
verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
server, therefore enforces revocation or not depending only on how its
libcurl was built. That difference is documented here rather than papered
over: this option turns the check on where the backend needs asking, and
setting it to false does not turn the check off on GnuTLS.
Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
Because http_options() is the collect_fn of a urlmatch config, the
per-URL form works with no further changes:
git config http.https://example.com/.sslVerifyStatus true
It defaults to false, and has to. The option is fail-closed: libcurl fails
verification when the server staples nothing at all, so turning this on
globally would break every remote that does not staple.
Leaving the default to libcurl is not an option either. The same
complaint was raised there in https://github.com/curl/curl/issues/15483
and closed as intentional ("Marked as enhancement since this was done on
purpose"), with the observation that stapling is expected to see less use
as Let's Encrypt drops OCSP support. If the check is to be reachable at
all, the lever has to come from the application.
If the TLS backend cannot check the staple, curl_easy_setopt() returns
CURLE_NOT_BUILT_IN. Fail loudly there rather than carrying on, since
silently not checking is precisely what this option exists to prevent.
CURLOPT_SSL_VERIFYSTATUS has been available since libcurl 7.41.0, well
below the 7.61.0 floor documented in INSTALL, so no version guard is
needed.
The new test exercises the fail-closed path, which needs no CA and no OCSP
responder: lib-httpd's server staples nothing, so enabling the option has
to turn a working fetch into a failing one. Verified against an unpatched
build, where exactly the two assertions that depend on the new option fail
and the three controls still pass, and against OpenSSL, GnuTLS and
mbedTLS-linked builds of libcurl.
Signed-off-by: Grayson Gordon <graysongordon1@gmail.com>
---
Documentation/config/http.adoc | 17 +++++++
http.c | 21 +++++++++
t/t5567-http-verify-status.sh | 72 +++++++++++++++++++++++++++++++
3 files changed, 110 insertions(+)
create mode 100755 t/t5567-http-verify-status.sh
diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 792a71b413..40b849bf7f 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -196,6 +196,23 @@ http.sslVerify::
over HTTPS. Defaults to true. Can be overridden by the
`GIT_SSL_NO_VERIFY` environment variable.
+http.sslVerifyStatus::
+ Whether to check the revocation status of the server
+ certificate using the stapled OCSP response supplied during
+ the TLS handshake ("OCSP stapling"). Defaults to false.
++
+This is fail-closed: if the server staples no response, verification
+fails. Set it per remote, e.g.
+`http.https://example.com/.sslVerifyStatus`, rather than globally.
++
+What it changes depends on the TLS backend libcurl was built against.
+An OpenSSL-linked build ignores a stapled response unless this is set.
+A GnuTLS-linked build consults the staple during ordinary certificate
+verification, so it already rejects a revoked certificate under
+`http.sslVerify` alone, and setting this to `false` does not disable
+that. Where a backend cannot check the staple at all, git fails with an
+error rather than continuing unchecked.
+
http.sslCert::
File containing the SSL certificate when fetching or pushing
over HTTPS. Can be overridden by the `GIT_SSL_CERT` environment
diff --git a/http.c b/http.c
index 5f0f42fb18..c1a66988e7 100644
--- a/http.c
+++ b/http.c
@@ -44,6 +44,7 @@ static CURL *curl_default;
char curl_errorstr[CURL_ERROR_SIZE];
static int curl_ssl_verify = -1;
+static int curl_ssl_verify_status;
static int curl_ssl_try;
static char *curl_http_version;
static char *ssl_cert;
@@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
curl_ssl_verify = git_config_bool(var, value);
return 0;
}
+ if (!strcmp("http.sslverifystatus", var)) {
+ curl_ssl_verify_status = git_config_bool(var, value);
+ return 0;
+ }
if (!strcmp("http.sslcipherlist", var))
return git_config_string(&ssl_cipherlist, var, value);
if (!strcmp("http.sslversion", var))
@@ -1131,6 +1136,22 @@ static CURL *get_curl_handle(void)
curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
}
+ /*
+ * Ask the TLS backend to check the certificate's revocation
+ * status via the stapled OCSP response. libcurl defaults this
+ * off, and no backend except GnuTLS consults the staple on its
+ * own, so without this git will happily accept a certificate
+ * whose own staple says it has been revoked.
+ *
+ * Off by default because it is fail-closed: a server that
+ * staples nothing fails verification outright, so enabling it
+ * globally would break every remote that does not staple.
+ */
+ if (curl_ssl_verify_status &&
+ curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
+ die(_("http.sslVerifyStatus is set, but the TLS backend of "
+ "this libcurl cannot verify certificate status"));
+
if (curl_http_version) {
long opt;
if (!get_curl_http_version_opt(curl_http_version, &opt)) {
diff --git a/t/t5567-http-verify-status.sh b/t/t5567-http-verify-status.sh
new file mode 100755
index 0000000000..c9167a05c2
--- /dev/null
+++ b/t/t5567-http-verify-status.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+test_description='http.sslVerifyStatus'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+LIB_HTTPD_SSL=t
+. "$TEST_DIRECTORY"/lib-httpd.sh
+start_httpd
+
+# The test server staples no OCSP response, and that is what makes this
+# testable without standing up a CA and a responder: http.sslVerifyStatus is
+# fail-closed, so turning it on has to turn a working fetch into a failing one.
+#
+# lib-httpd.sh exports GIT_SSL_NO_VERIFY for its self-signed certificate. In
+# libcurl the status check is independent of peer verification, so it still
+# applies here.
+
+test_expect_success 'setup repository' '
+ echo content >file &&
+ git add file &&
+ git commit -m one
+'
+
+test_expect_success 'create http-accessible bare repository' '
+ git init --bare "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
+ git remote add public "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
+ git push public main:main
+'
+
+# A TLS backend that cannot check the staple makes curl_easy_setopt() fail,
+# which http.c reports with a distinct message. Skip in that case rather than
+# reporting a failure that really means "this libcurl was built differently".
+# Any other failure leaves the prerequisite satisfied on purpose, so a broken
+# server makes the tests below fail loudly instead of silently vanishing.
+test_lazy_prereq SSL_VERIFYSTATUS '
+ git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git" 2>err
+ ! grep "cannot verify certificate status" err
+'
+
+test_expect_success 'ls-remote succeeds with http.sslVerifyStatus unset' '
+ git ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
+ test_must_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
+ git -c http.sslVerifyStatus=false \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL configuration applies to a matching URL' '
+ test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL configuration is not applied to other URLs' '
+ git -c "http.https://example.com/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_done
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-11 17:02 [PATCH] http: add http.sslVerifyStatus to check stapled OCSP responses graysongordon-gl
@ 2026-08-11 19:28 ` Junio C Hamano
2026-08-11 20:44 ` [PATCH v2] " graysongordon-gl
1 sibling, 0 replies; 16+ messages in thread
From: Junio C Hamano @ 2026-08-11 19:28 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git, peff, avarab, ps
graysongordon-gl <graysongordon1@gmail.com> writes:
> CURLOPT_SSL_VERIFYSTATUS has been available since libcurl 7.41.0, well
> below the 7.61.0 floor documented in INSTALL, so no version guard is
> needed.
Good to see that the author paid extra attention to compatibility.
> + /*
> + * Ask the TLS backend to check the certificate's revocation
> + * status via the stapled OCSP response. libcurl defaults this
> + * off, and no backend except GnuTLS consults the staple on its
> + * own, so without this git will happily accept a certificate
> + * whose own staple says it has been revoked.
> + *
> + * Off by default because it is fail-closed: a server that
> + * staples nothing fails verification outright, so enabling it
> + * globally would break every remote that does not staple.
> + */
The comment may not be telling any lies per se, but it is dubious
that this belongs here as an in-code comment. Developers hunting a
bug they suspect this setting might have caused will need access to
this information, and they can access it by running 'git blame' to
locate the commit that introduced the code. As long as a solid
commit log message explains how you arrived at various design
decisions (such as 'off by default because'), they can use that as a
starting point. For other developers hunting different bugs or
trying to add their own enhancements, the comment is a mere
distraction.
> + if (curl_ssl_verify_status &&
> + curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
> + die(_("http.sslVerifyStatus is set, but the TLS backend of "
> + "this libcurl cannot verify certificate status"));
> +
Thanks.
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v2] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-11 17:02 [PATCH] http: add http.sslVerifyStatus to check stapled OCSP responses graysongordon-gl
2026-08-11 19:28 ` Junio C Hamano
@ 2026-08-11 20:44 ` graysongordon-gl
2026-08-12 6:25 ` Patrick Steinhardt
2026-08-12 14:17 ` Junio C Hamano
1 sibling, 2 replies; 16+ messages in thread
From: graysongordon-gl @ 2026-08-11 20:44 UTC (permalink / raw)
To: git; +Cc: gitster, peff, avarab, ps, Grayson Gordon
From: Grayson Gordon <graysongordon1@gmail.com>
git asks libcurl to verify the peer certificate and the hostname, but it
never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
TLS extension is never requested and any stapled OCSP response the server
does send is ignored.
On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
response to the application and takes no view on it:
SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
whether the returned OCSP response(s) are acceptable or not", and libcurl
only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
will fetch from a server whose own staple says its certificate has been
revoked.
A GnuTLS-linked build behaves differently, and the difference does not
come from curl. GnuTLS consults a stapled response inside
gnutls_certificate_verify_peers(), so the failure surfaces through the
verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
server, therefore enforces revocation or not depending only on how its
libcurl was built. That difference is documented here rather than papered
over: this option turns the check on where the backend needs asking, and
setting it to false does not turn the check off on GnuTLS.
Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
Because http_options() is the collect_fn of a urlmatch config, the
per-URL form works with no further changes:
git config http.https://example.com/.sslVerifyStatus true
It defaults to false, and has to. The option is fail-closed: libcurl fails
verification when the server staples nothing at all, so turning this on
globally would break every remote that does not staple.
Leaving the default to libcurl is not an option either. The same
complaint was raised there in https://github.com/curl/curl/issues/15483
and closed as intentional ("Marked as enhancement since this was done on
purpose"), with the observation that stapling is expected to see less use
as Let's Encrypt drops OCSP support. If the check is to be reachable at
all, the lever has to come from the application.
If the TLS backend cannot check the staple, curl_easy_setopt() returns
CURLE_NOT_BUILT_IN. Fail loudly there rather than carrying on, since
silently not checking is precisely what this option exists to prevent.
CURLOPT_SSL_VERIFYSTATUS has been available since libcurl 7.41.0, well
below the 7.61.0 floor documented in INSTALL, so no version guard is
needed.
The new test exercises the fail-closed path, which needs no CA and no OCSP
responder: lib-httpd's server staples nothing, so enabling the option has
to turn a working fetch into a failing one. Verified against an unpatched
build, where exactly the two assertions that depend on the new option fail
and the three controls still pass, and against OpenSSL, GnuTLS and
mbedTLS-linked builds of libcurl.
Signed-off-by: Grayson Gordon <graysongordon1@gmail.com>
---
v2: drop the block comment above the setopt. What it explained (why the
check is needed, and why the default is false) is already in the
commit message, which is where "git blame" leads anyone debugging
this. No code change otherwise.
Documentation/config/http.adoc | 17 +++++++
http.c | 10 ++++
t/t5567-http-verify-status.sh | 72 +++++++++++++++++++++++++++++++
3 files changed, 99 insertions(+)
create mode 100755 t/t5567-http-verify-status.sh
diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 792a71b413..40b849bf7f 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -196,6 +196,23 @@ http.sslVerify::
over HTTPS. Defaults to true. Can be overridden by the
`GIT_SSL_NO_VERIFY` environment variable.
+http.sslVerifyStatus::
+ Whether to check the revocation status of the server
+ certificate using the stapled OCSP response supplied during
+ the TLS handshake ("OCSP stapling"). Defaults to false.
++
+This is fail-closed: if the server staples no response, verification
+fails. Set it per remote, e.g.
+`http.https://example.com/.sslVerifyStatus`, rather than globally.
++
+What it changes depends on the TLS backend libcurl was built against.
+An OpenSSL-linked build ignores a stapled response unless this is set.
+A GnuTLS-linked build consults the staple during ordinary certificate
+verification, so it already rejects a revoked certificate under
+`http.sslVerify` alone, and setting this to `false` does not disable
+that. Where a backend cannot check the staple at all, git fails with an
+error rather than continuing unchecked.
+
http.sslCert::
File containing the SSL certificate when fetching or pushing
over HTTPS. Can be overridden by the `GIT_SSL_CERT` environment
diff --git a/http.c b/http.c
index 5f0f42fb18..c1a66988e7 100644
--- a/http.c
+++ b/http.c
@@ -44,6 +44,7 @@ static CURL *curl_default;
char curl_errorstr[CURL_ERROR_SIZE];
static int curl_ssl_verify = -1;
+static int curl_ssl_verify_status;
static int curl_ssl_try;
static char *curl_http_version;
static char *ssl_cert;
@@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
curl_ssl_verify = git_config_bool(var, value);
return 0;
}
+ if (!strcmp("http.sslverifystatus", var)) {
+ curl_ssl_verify_status = git_config_bool(var, value);
+ return 0;
+ }
if (!strcmp("http.sslcipherlist", var))
return git_config_string(&ssl_cipherlist, var, value);
if (!strcmp("http.sslversion", var))
@@ -1133,6 +1138,11 @@ static CURL *get_curl_handle(void)
curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
}
+ if (curl_ssl_verify_status &&
+ curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
+ die(_("http.sslVerifyStatus is set, but the TLS backend of "
+ "this libcurl cannot verify certificate status"));
+
if (curl_http_version) {
long opt;
if (!get_curl_http_version_opt(curl_http_version, &opt)) {
diff --git a/t/t5567-http-verify-status.sh b/t/t5567-http-verify-status.sh
new file mode 100755
index 0000000000..c9167a05c2
--- /dev/null
+++ b/t/t5567-http-verify-status.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+test_description='http.sslVerifyStatus'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+LIB_HTTPD_SSL=t
+. "$TEST_DIRECTORY"/lib-httpd.sh
+start_httpd
+
+# The test server staples no OCSP response, and that is what makes this
+# testable without standing up a CA and a responder: http.sslVerifyStatus is
+# fail-closed, so turning it on has to turn a working fetch into a failing one.
+#
+# lib-httpd.sh exports GIT_SSL_NO_VERIFY for its self-signed certificate. In
+# libcurl the status check is independent of peer verification, so it still
+# applies here.
+
+test_expect_success 'setup repository' '
+ echo content >file &&
+ git add file &&
+ git commit -m one
+'
+
+test_expect_success 'create http-accessible bare repository' '
+ git init --bare "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
+ git remote add public "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
+ git push public main:main
+'
+
+# A TLS backend that cannot check the staple makes curl_easy_setopt() fail,
+# which http.c reports with a distinct message. Skip in that case rather than
+# reporting a failure that really means "this libcurl was built differently".
+# Any other failure leaves the prerequisite satisfied on purpose, so a broken
+# server makes the tests below fail loudly instead of silently vanishing.
+test_lazy_prereq SSL_VERIFYSTATUS '
+ git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git" 2>err
+ ! grep "cannot verify certificate status" err
+'
+
+test_expect_success 'ls-remote succeeds with http.sslVerifyStatus unset' '
+ git ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
+ test_must_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
+ git -c http.sslVerifyStatus=false \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL configuration applies to a matching URL' '
+ test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL configuration is not applied to other URLs' '
+ git -c "http.https://example.com/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_done
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v2] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-11 20:44 ` [PATCH v2] " graysongordon-gl
@ 2026-08-12 6:25 ` Patrick Steinhardt
2026-08-12 15:53 ` Grayson Gordon
2026-08-12 14:17 ` Junio C Hamano
1 sibling, 1 reply; 16+ messages in thread
From: Patrick Steinhardt @ 2026-08-12 6:25 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git, gitster, peff, avarab
On Tue, Aug 11, 2026 at 04:44:07PM -0400, graysongordon-gl wrote:
> From: Grayson Gordon <graysongordon1@gmail.com>
>
> git asks libcurl to verify the peer certificate and the hostname, but it
> never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
> TLS extension is never requested and any stapled OCSP response the server
> does send is ignored.
>
> On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
> response to the application and takes no view on it:
> SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
> whether the returned OCSP response(s) are acceptable or not", and libcurl
> only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
> will fetch from a server whose own staple says its certificate has been
> revoked.
>
> A GnuTLS-linked build behaves differently, and the difference does not
> come from curl. GnuTLS consults a stapled response inside
> gnutls_certificate_verify_peers(), so the failure surfaces through the
> verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
> not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
Nit: this is arguably not the same git, as it links against different
libraries. It is not exactly unexpected that using different
dependencies may cause different behaviour, even though we should of
course try to minimize the differences.
> server, therefore enforces revocation or not depending only on how its
> libcurl was built. That difference is documented here rather than papered
> over: this option turns the check on where the backend needs asking, and
> setting it to false does not turn the check off on GnuTLS.
>
> Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
> Because http_options() is the collect_fn of a urlmatch config, the
> per-URL form works with no further changes:
>
> git config http.https://example.com/.sslVerifyStatus true
>
> It defaults to false, and has to. The option is fail-closed: libcurl fails
> verification when the server staples nothing at all, so turning this on
> globally would break every remote that does not staple.
>
> Leaving the default to libcurl is not an option either. The same
> complaint was raised there in https://github.com/curl/curl/issues/15483
> and closed as intentional ("Marked as enhancement since this was done on
> purpose"), with the observation that stapling is expected to see less use
> as Let's Encrypt drops OCSP support. If the check is to be reachable at
> all, the lever has to come from the application.
Okay. One could make the argument that we shouldn't add support for OCSP
either if it's being phased out now. But I assume there's still going to
be enough servers out there that do use it.
The big question to me is why we want to have this change in the first
place. It doesn't help to address the behaviour difference between
GnuTLS and OpenSSL: if set to "false" OpenSSL would continue to ignore
OCSP, whereas GnuTLS would still honor it. If set to "true", OpenSSL
would fail closed, whereas GnuTLS would still behave the same as before.
So nothing really changes here, unless I misunderstand something.
We don't really gain security, either, because the setting is disabled
by default and can only be enabled host-by-host. I doubt anybody out
there is really going to do that though, and consequently we haven't
really made the world a more secure place :/
So is there any specific use case that you're after? Who exactly is this
new feature for?
Thanks!
Patrick
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v2] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-11 20:44 ` [PATCH v2] " graysongordon-gl
2026-08-12 6:25 ` Patrick Steinhardt
@ 2026-08-12 14:17 ` Junio C Hamano
2026-08-12 18:25 ` [PATCH v3] " graysongordon-gl
1 sibling, 1 reply; 16+ messages in thread
From: Junio C Hamano @ 2026-08-12 14:17 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git, peff, avarab, ps
graysongordon-gl <graysongordon1@gmail.com> writes:
> Documentation/config/http.adoc | 17 +++++++
> http.c | 10 ++++
> t/t5567-http-verify-status.sh | 72 +++++++++++++++++++++++++++++++
> 3 files changed, 99 insertions(+)
> create mode 100755 t/t5567-http-verify-status.sh
Hmph, if we need a brand new script, please make sure the 4-digit
number is not taken, not just in the sources to released versions
but by other topics that are in flight.
$ git show origin/seen:t | grep t5567
should be empty, but it is not. It seems mm/lib-httpd-cgi-safe topic
grabbed it.
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v2] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-12 6:25 ` Patrick Steinhardt
@ 2026-08-12 15:53 ` Grayson Gordon
0 siblings, 0 replies; 16+ messages in thread
From: Grayson Gordon @ 2026-08-12 15:53 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, gitster, peff, avarab
Patrick,
Thank you for reviewing my submission!
I understand the "nit" you're describing: software can't exactly be
called the same thing if it is built against different libraries,
which in turn creates an opportunity for different behaviors. I agree
with your follow-up that we should aim to maintain consistency despite
those library choices. The benefits of the Principle of Least
Astonishment are well established, and I would argue that users
reasonably expect Git to behave consistently regardless of the
underlying TLS library.
I can provide additional context to motivate this change. Like you, I
currently work for GitLab, but as part of the Professional Services
organization, which works directly with customers deploying the
software in their environments for production use. I am supporting a
government customer whose servers use OCSP stapling. There are many
such government and government-adjacent customers that utilize
certificates issued by US Department of Defense PKI CAs, which have
published policies that explicitly outline support for OCSP:
https://dl.dod.cyber.mil/wp-content/uploads/pki-pke/pdf/Unclass-DoD_X.509_Certificate_Policy_v10.7_Jun_3_21.pdf.
Many DoD PKI CAs serve certificates with stapled OCSP responses today
and can reasonably be expected to continue to do so until there is a
DoD-wide policy change.
A related bug in GnuTLS has affected my customer in their current
production environment, preventing them from being able to push-mirror
to repositories on remotes whose servers use OCSP stapling. The
push-mirror failure is what prompted my initial investigation into
this issue.
Original GnuTLS issue:
https://gitlab.com/gnutls/gnutls/-/work_items/1372, resolved in GnuTLS
3.8.8.
GitLab issue on the Cloud Native GitLab build that resolves this
behavior in GitLab's default Helm chart base image:
https://gitlab.com/gitlab-org/build/CNG/-/work_items/2374#note_3653072099
GitLab ships its Helm charts with two primary "flavors": one based on
Debian and the other on UBI. On Debian, the default SSL backend is
GnuTLS, and the aforementioned issues resolve the problem. On UBI, the
default backend is OpenSSL, and this issue surfaces. For my government
customers who need FIPS, switching to the UBI-based image is the
long-term path forward:
https://gitlab.com/graysongordon-gl/gitaly-tls-experiments/-/blob/main/docs/FIPS-AND-THE-TLS-BACKEND.md?ref_type=heads.
To be more explicit, OpenSSL-linked Git binaries are the default case
for many government customers, and those customers frequently
interface with Git servers that use this type of certificate
revocation mechanism.
In summary, there are many instances of Git servers serving a large
base of developers working on government-related software that are
impacted by this issue and need this functionality. These users have
experienced the pain and confusion of this behavior being broken
firsthand in downstream applications and have brought the issue to me.
These customers care that their Git clients respect certificate
revocation when it occurs, whether from their development machines or
through service-to-service communications over Git on platforms like
GitLab. They interface with these kinds of certificates frequently and
will continue to do so, which warrants the inclusion of this flag. The
benefit they would receive is correct validation of a remote's
certificate.
As it stands today, users leveraging OpenSSL-linked Git binaries can
receive a response indicating that the certificate is valid even when
the stapled OCSP response indicates that the certificate has been
revoked. I think there is a reasonable case that this could qualify as
a low-to-medium severity CVE.
The threat model is:
An attacker steals the private key of a Git server whose certificate
is accompanied by an OCSP-stapled response.
The breach is detected, and the certificate authority revokes the certificate.
Despite the revocation, Git clients continue to accept the certificate
and push/pull code from a malicious Git server impersonating the
legitimate server.
A malicious actor could leverage this to facilitate the exfiltration
of an organization's Git data.
Similar CVEs against libcurl include:
https://curl.se/mail/lib-2026-04/0036.html,
https://curl.se/docs/CVE-2024-0853.html
In addition, the attacker would need a mechanism for intercepting or
redirecting the victim's connection to the Git server, hence this not
being a higher-severity issue. However, I think that in the context of
DoD systems, this is sufficiently dangerous to warrant remediation,
and this patch provides that capability.
On the GitLab side, we already have mechanisms for per-remote
configuration values to be passed, and integrating this would not be a
monumental lift.
Thank you,
Grayson
On Wed, Aug 12, 2026 at 2:25 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Tue, Aug 11, 2026 at 04:44:07PM -0400, graysongordon-gl wrote:
> > From: Grayson Gordon <graysongordon1@gmail.com>
> >
> > git asks libcurl to verify the peer certificate and the hostname, but it
> > never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
> > TLS extension is never requested and any stapled OCSP response the server
> > does send is ignored.
> >
> > On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
> > response to the application and takes no view on it:
> > SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
> > whether the returned OCSP response(s) are acceptable or not", and libcurl
> > only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
> > will fetch from a server whose own staple says its certificate has been
> > revoked.
> >
> > A GnuTLS-linked build behaves differently, and the difference does not
> > come from curl. GnuTLS consults a stapled response inside
> > gnutls_certificate_verify_peers(), so the failure surfaces through the
> > verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
> > not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
>
> Nit: this is arguably not the same git, as it links against different
> libraries. It is not exactly unexpected that using different
> dependencies may cause different behaviour, even though we should of
> course try to minimize the differences.
>
> > server, therefore enforces revocation or not depending only on how its
> > libcurl was built. That difference is documented here rather than papered
> > over: this option turns the check on where the backend needs asking, and
> > setting it to false does not turn the check off on GnuTLS.
> >
> > Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
> > Because http_options() is the collect_fn of a urlmatch config, the
> > per-URL form works with no further changes:
> >
> > git config http.https://example.com/.sslVerifyStatus true
> >
> > It defaults to false, and has to. The option is fail-closed: libcurl fails
> > verification when the server staples nothing at all, so turning this on
> > globally would break every remote that does not staple.
> >
> > Leaving the default to libcurl is not an option either. The same
> > complaint was raised there in https://github.com/curl/curl/issues/15483
> > and closed as intentional ("Marked as enhancement since this was done on
> > purpose"), with the observation that stapling is expected to see less use
> > as Let's Encrypt drops OCSP support. If the check is to be reachable at
> > all, the lever has to come from the application.
>
> Okay. One could make the argument that we shouldn't add support for OCSP
> either if it's being phased out now. But I assume there's still going to
> be enough servers out there that do use it.
>
> The big question to me is why we want to have this change in the first
> place. It doesn't help to address the behaviour difference between
> GnuTLS and OpenSSL: if set to "false" OpenSSL would continue to ignore
> OCSP, whereas GnuTLS would still honor it. If set to "true", OpenSSL
> would fail closed, whereas GnuTLS would still behave the same as before.
> So nothing really changes here, unless I misunderstand something.
>
> We don't really gain security, either, because the setting is disabled
> by default and can only be enabled host-by-host. I doubt anybody out
> there is really going to do that though, and consequently we haven't
> really made the world a more secure place :/
>
> So is there any specific use case that you're after? Who exactly is this
> new feature for?
>
> Thanks!
>
> Patrick
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v3] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-12 14:17 ` Junio C Hamano
@ 2026-08-12 18:25 ` graysongordon-gl
2026-08-12 21:34 ` Junio C Hamano
0 siblings, 1 reply; 16+ messages in thread
From: graysongordon-gl @ 2026-08-12 18:25 UTC (permalink / raw)
To: git; +Cc: gitster, peff, avarab, ps, Grayson Gordon
From: Grayson Gordon <graysongordon1@gmail.com>
git asks libcurl to verify the peer certificate and the hostname, but it
never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
TLS extension is never requested and any stapled OCSP response the server
does send is ignored.
On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
response to the application and takes no view on it:
SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
whether the returned OCSP response(s) are acceptable or not", and libcurl
only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
will fetch from a server whose own staple says its certificate has been
revoked.
A GnuTLS-linked build behaves differently, and the difference does not
come from curl. GnuTLS consults a stapled response inside
gnutls_certificate_verify_peers(), so the failure surfaces through the
verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
server, therefore enforces revocation or not depending only on how its
libcurl was built. That difference is documented here rather than papered
over: this option turns the check on where the backend needs asking, and
setting it to false does not turn the check off on GnuTLS.
Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
Because http_options() is the collect_fn of a urlmatch config, the
per-URL form works with no further changes:
git config http.https://example.com/.sslVerifyStatus true
It defaults to false, and has to. The option is fail-closed: libcurl fails
verification when the server staples nothing at all, so turning this on
globally would break every remote that does not staple.
Leaving the default to libcurl is not an option either. The same
complaint was raised there in https://github.com/curl/curl/issues/15483
and closed as intentional ("Marked as enhancement since this was done on
purpose"), with the observation that stapling is expected to see less use
as Let's Encrypt drops OCSP support. If the check is to be reachable at
all, the lever has to come from the application.
If the TLS backend cannot check the staple, curl_easy_setopt() returns
CURLE_NOT_BUILT_IN. Fail loudly there rather than carrying on, since
silently not checking is precisely what this option exists to prevent.
CURLOPT_SSL_VERIFYSTATUS has been available since libcurl 7.41.0, well
below the 7.61.0 floor documented in INSTALL, so no version guard is
needed.
The new test exercises the fail-closed path, which needs no CA and no OCSP
responder: lib-httpd's server staples nothing, so enabling the option has
to turn a working fetch into a failing one. Verified against an unpatched
build, where exactly the two assertions that depend on the new option fail
and the three controls still pass, and against OpenSSL, GnuTLS and
mbedTLS-linked builds of libcurl.
Signed-off-by: Grayson Gordon <graysongordon1@gmail.com>
---
v3: rename the test from t5567 to t5568. t5567 is taken on 'seen' by
mm/lib-httpd-cgi-safe. t5568 is free on master, next, seen, jch and
maint as of b9720e4723, and sits next to the other http tests. No
other change.
v2: drop the block comment above the setopt. What it explained (why the
check is needed, and why the default is false) is already in the
commit message, which is where "git blame" leads anyone debugging
this. No code change otherwise.
Documentation/config/http.adoc | 17 +++++++
http.c | 10 ++++
t/t5568-http-verify-status.sh | 72 +++++++++++++++++++++++++++++++
3 files changed, 99 insertions(+)
create mode 100755 t/t5568-http-verify-status.sh
diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 792a71b413..40b849bf7f 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -196,6 +196,23 @@ http.sslVerify::
over HTTPS. Defaults to true. Can be overridden by the
`GIT_SSL_NO_VERIFY` environment variable.
+http.sslVerifyStatus::
+ Whether to check the revocation status of the server
+ certificate using the stapled OCSP response supplied during
+ the TLS handshake ("OCSP stapling"). Defaults to false.
++
+This is fail-closed: if the server staples no response, verification
+fails. Set it per remote, e.g.
+`http.https://example.com/.sslVerifyStatus`, rather than globally.
++
+What it changes depends on the TLS backend libcurl was built against.
+An OpenSSL-linked build ignores a stapled response unless this is set.
+A GnuTLS-linked build consults the staple during ordinary certificate
+verification, so it already rejects a revoked certificate under
+`http.sslVerify` alone, and setting this to `false` does not disable
+that. Where a backend cannot check the staple at all, git fails with an
+error rather than continuing unchecked.
+
http.sslCert::
File containing the SSL certificate when fetching or pushing
over HTTPS. Can be overridden by the `GIT_SSL_CERT` environment
diff --git a/http.c b/http.c
index 5f0f42fb18..c1a66988e7 100644
--- a/http.c
+++ b/http.c
@@ -44,6 +44,7 @@ static CURL *curl_default;
char curl_errorstr[CURL_ERROR_SIZE];
static int curl_ssl_verify = -1;
+static int curl_ssl_verify_status;
static int curl_ssl_try;
static char *curl_http_version;
static char *ssl_cert;
@@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
curl_ssl_verify = git_config_bool(var, value);
return 0;
}
+ if (!strcmp("http.sslverifystatus", var)) {
+ curl_ssl_verify_status = git_config_bool(var, value);
+ return 0;
+ }
if (!strcmp("http.sslcipherlist", var))
return git_config_string(&ssl_cipherlist, var, value);
if (!strcmp("http.sslversion", var))
@@ -1133,6 +1138,11 @@ static CURL *get_curl_handle(void)
curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
}
+ if (curl_ssl_verify_status &&
+ curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
+ die(_("http.sslVerifyStatus is set, but the TLS backend of "
+ "this libcurl cannot verify certificate status"));
+
if (curl_http_version) {
long opt;
if (!get_curl_http_version_opt(curl_http_version, &opt)) {
diff --git a/t/t5568-http-verify-status.sh b/t/t5568-http-verify-status.sh
new file mode 100755
index 0000000000..c9167a05c2
--- /dev/null
+++ b/t/t5568-http-verify-status.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+test_description='http.sslVerifyStatus'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+LIB_HTTPD_SSL=t
+. "$TEST_DIRECTORY"/lib-httpd.sh
+start_httpd
+
+# The test server staples no OCSP response, and that is what makes this
+# testable without standing up a CA and a responder: http.sslVerifyStatus is
+# fail-closed, so turning it on has to turn a working fetch into a failing one.
+#
+# lib-httpd.sh exports GIT_SSL_NO_VERIFY for its self-signed certificate. In
+# libcurl the status check is independent of peer verification, so it still
+# applies here.
+
+test_expect_success 'setup repository' '
+ echo content >file &&
+ git add file &&
+ git commit -m one
+'
+
+test_expect_success 'create http-accessible bare repository' '
+ git init --bare "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
+ git remote add public "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
+ git push public main:main
+'
+
+# A TLS backend that cannot check the staple makes curl_easy_setopt() fail,
+# which http.c reports with a distinct message. Skip in that case rather than
+# reporting a failure that really means "this libcurl was built differently".
+# Any other failure leaves the prerequisite satisfied on purpose, so a broken
+# server makes the tests below fail loudly instead of silently vanishing.
+test_lazy_prereq SSL_VERIFYSTATUS '
+ git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git" 2>err
+ ! grep "cannot verify certificate status" err
+'
+
+test_expect_success 'ls-remote succeeds with http.sslVerifyStatus unset' '
+ git ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
+ test_must_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
+ git -c http.sslVerifyStatus=false \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL configuration applies to a matching URL' '
+ test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL configuration is not applied to other URLs' '
+ git -c "http.https://example.com/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_done
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v3] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-12 18:25 ` [PATCH v3] " graysongordon-gl
@ 2026-08-12 21:34 ` Junio C Hamano
2026-08-13 16:06 ` Junio C Hamano
0 siblings, 1 reply; 16+ messages in thread
From: Junio C Hamano @ 2026-08-12 21:34 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git, peff, avarab, ps
graysongordon-gl <graysongordon1@gmail.com> writes:
> v3: rename the test from t5567 to t5568. t5567 is taken on 'seen' by
> mm/lib-httpd-cgi-safe. t5568 is free on master, next, seen, jch and
> maint as of b9720e4723, and sits next to the other http tests. No
> other change.
I thought I first asked whether we need a new script before
suggesting moving it out of the way because 't5567' was already
taken. It is much better not to waste a scarce, shared resource
such as a test number, and doing so avoids breaking the build if we
are not careful.
If we really need to add a new script, you would need to squash in
at least a patch like this to avoid breaking Meson-based builds.
t/meson.build | 1 +
1 file changed, 1 insertion(+)
diff --git i/t/meson.build w/t/meson.build
index 3219264fe7..3d68f67680 100644
--- i/t/meson.build
+++ w/t/meson.build
@@ -707,6 +707,7 @@ integration_tests = [
't5564-http-proxy.sh',
't5565-push-multiple.sh',
't5566-push-group.sh',
+ 't5568-http-verify-status.sh',
't5570-git-daemon.sh',
't5571-pre-push-hook.sh',
't5572-pull-submodule.sh',
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v3] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-12 21:34 ` Junio C Hamano
@ 2026-08-13 16:06 ` Junio C Hamano
2026-08-17 18:52 ` [PATCH v4] " graysongordon-gl
2026-08-18 19:37 ` [PATCH v5] " graysongordon-gl
0 siblings, 2 replies; 16+ messages in thread
From: Junio C Hamano @ 2026-08-13 16:06 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git, peff, avarab, ps
Junio C Hamano <gitster@pobox.com> writes:
> graysongordon-gl <graysongordon1@gmail.com> writes:
>
>> v3: rename the test from t5567 to t5568. t5567 is taken on 'seen' by
>> mm/lib-httpd-cgi-safe. t5568 is free on master, next, seen, jch and
>> maint as of b9720e4723, and sits next to the other http tests. No
>> other change.
>
> I thought I first asked whether we need a new script before
> suggesting moving it out of the way because 't5567' was already
> taken. It is much better not to waste a scarce, shared resource
> such as a test number, and doing so avoids breaking the build if we
> are not careful.
>
> If we really need to add a new script, you would need to squash in
> at least a patch like this to avoid breaking Meson-based builds.
>
>
> t/meson.build | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git i/t/meson.build w/t/meson.build
> index 3219264fe7..3d68f67680 100644
> --- i/t/meson.build
> +++ w/t/meson.build
> @@ -707,6 +707,7 @@ integration_tests = [
> 't5564-http-proxy.sh',
> 't5565-push-multiple.sh',
> 't5566-push-group.sh',
> + 't5568-http-verify-status.sh',
> 't5570-git-daemon.sh',
> 't5571-pre-push-hook.sh',
> 't5572-pull-submodule.sh',
BTW, exit status of ls-remote is lost without the following:
diff --git a/t/t5568-http-verify-status.sh b/t/t5568-http-verify-status.sh
index c9167a05c2..7ba70fc8af 100755
--- a/t/t5568-http-verify-status.sh
+++ b/t/t5568-http-verify-status.sh
@@ -38,7 +38,7 @@ test_expect_success 'create http-accessible bare repository' '
# server makes the tests below fail loudly instead of silently vanishing.
test_lazy_prereq SSL_VERIFYSTATUS '
git -c http.sslVerifyStatus=true \
- ls-remote "$HTTPD_URL/smart/repo.git" 2>err
+ ls-remote "$HTTPD_URL/smart/repo.git" 2>err &&
! grep "cannot verify certificate status" err
'
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH v4] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-13 16:06 ` Junio C Hamano
@ 2026-08-17 18:52 ` graysongordon-gl
2026-08-17 19:19 ` Junio C Hamano
2026-08-18 7:50 ` Patrick Steinhardt
2026-08-18 19:37 ` [PATCH v5] " graysongordon-gl
1 sibling, 2 replies; 16+ messages in thread
From: graysongordon-gl @ 2026-08-17 18:52 UTC (permalink / raw)
To: gitster; +Cc: git, Grayson Gordon
From: Grayson Gordon <graysongordon1@gmail.com>
git asks libcurl to verify the peer certificate and the hostname, but it
never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
TLS extension is never requested and any stapled OCSP response the server
does send is ignored.
On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
response to the application and takes no view on it:
SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
whether the returned OCSP response(s) are acceptable or not", and libcurl
only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
will fetch from a server whose own staple says its certificate has been
revoked.
A GnuTLS-linked build behaves differently, and the difference does not
come from curl. GnuTLS consults a stapled response inside
gnutls_certificate_verify_peers(), so the failure surfaces through the
verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
server, therefore enforces revocation or not depending only on how its
libcurl was built. That difference is documented here rather than papered
over: this option turns the check on where the backend needs asking, and
setting it to false does not turn the check off on GnuTLS.
Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
Because http_options() is the collect_fn of a urlmatch config, the
per-URL form works with no further changes:
git config http.https://example.com/.sslVerifyStatus true
It defaults to false, and has to. The option is fail-closed: libcurl fails
verification when the server staples nothing at all, so turning this on
globally would break every remote that does not staple.
Leaving the default to libcurl is not an option either. The same
complaint was raised there in https://github.com/curl/curl/issues/15483
and closed as intentional ("Marked as enhancement since this was done on
purpose"), with the observation that stapling is expected to see less use
as Let's Encrypt drops OCSP support. If the check is to be reachable at
all, the lever has to come from the application.
If the TLS backend cannot check the staple, curl_easy_setopt() returns
CURLE_NOT_BUILT_IN. Fail loudly there rather than carrying on, since
silently not checking is precisely what this option exists to prevent.
CURLOPT_SSL_VERIFYSTATUS has been available since libcurl 7.41.0, well
below the 7.61.0 floor documented in INSTALL, so no version guard is
needed.
The tests go in t5551 and run only in its https pass, which t5559 provides
by sourcing t5551 with LIB_HTTPD_SSL set; that is the only https server
the suite has. They exercise the fail-closed path, which needs no CA and
no OCSP responder: lib-httpd's server staples nothing, so enabling the
option has to turn a working fetch into a failing one. Verified against an
unpatched build, where exactly the two assertions that depend on the new
option fail and the two controls still pass, and against OpenSSL, GnuTLS
and mbedTLS-linked builds of libcurl.
Signed-off-by: Grayson Gordon <graysongordon1@gmail.com>
---
v4: drop the new test script. The four tests now live in t5551, keyed on
$HTTPD_PROTO so they run in the https pass (t5559) only.
To answer the question I skipped past in v3: no, we do not need a new
script. t5559 is t5551 run with LIB_HTTPD_SSL set, and it is the only
https server the suite has, so a new script would have had to stand up a
second one to reach the same place. Nothing is left over from t5567 or
t5568 and no test number is spent.
That also means the t/meson.build hunk is not squashed in. t5551 is
already listed there. Adding t5568 to the list now would break configure
the other way round, since the list is checked against ls in both
directions and errors with "Test files configured, but not found".
On the lost exit status: changed, but to test_might_fail rather than a
bare &&. The ls-remote in the prerequisite is expected to fail, that is
the premise of the test, so && short-circuits on the expected failure and
leaves the prerequisite unsatisfied. Run against the https server both
ways:
bare && ok 51 # skip http.sslVerifyStatus=true fails
without a staple (missing SSL_VERIFYSTATUS)
test_might_fail ok 51 - http.sslVerifyStatus=true fails
without a staple
The first still reports "passed all 61 test(s)", which is the failure mode
the prerequisite was written to avoid. test_might_fail keeps the chain
intact and says the status is ignored on purpose.
Also dropped the "ls-remote succeeds with http.sslVerifyStatus unset"
test. It was a control for the standalone script, and in t5551 the
surrounding tests already exercise that URL throughout.
The rationale that sat in an in-code comment in v3 is in the log now, per
the earlier review.
Verified: t5551 over plain http and t5559 over https both pass all 61
tests, with the four new ones skipping on the former and running on the
latter.
Documentation/config/http.adoc | 17 +++++++++++++++++
http.c | 10 ++++++++++
t/t5551-http-fetch-smart.sh | 29 +++++++++++++++++++++++++++++
3 files changed, 56 insertions(+)
diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 792a71b413..40b849bf7f 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -196,6 +196,23 @@ http.sslVerify::
over HTTPS. Defaults to true. Can be overridden by the
`GIT_SSL_NO_VERIFY` environment variable.
+http.sslVerifyStatus::
+ Whether to check the revocation status of the server
+ certificate using the stapled OCSP response supplied during
+ the TLS handshake ("OCSP stapling"). Defaults to false.
++
+This is fail-closed: if the server staples no response, verification
+fails. Set it per remote, e.g.
+`http.https://example.com/.sslVerifyStatus`, rather than globally.
++
+What it changes depends on the TLS backend libcurl was built against.
+An OpenSSL-linked build ignores a stapled response unless this is set.
+A GnuTLS-linked build consults the staple during ordinary certificate
+verification, so it already rejects a revoked certificate under
+`http.sslVerify` alone, and setting this to `false` does not disable
+that. Where a backend cannot check the staple at all, git fails with an
+error rather than continuing unchecked.
+
http.sslCert::
File containing the SSL certificate when fetching or pushing
over HTTPS. Can be overridden by the `GIT_SSL_CERT` environment
diff --git a/http.c b/http.c
index caccf2108e..94f8dd817a 100644
--- a/http.c
+++ b/http.c
@@ -44,6 +44,7 @@ static CURL *curl_default;
char curl_errorstr[CURL_ERROR_SIZE];
static int curl_ssl_verify = -1;
+static int curl_ssl_verify_status;
static int curl_ssl_try;
static char *curl_http_version;
static char *ssl_cert;
@@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
curl_ssl_verify = git_config_bool(var, value);
return 0;
}
+ if (!strcmp("http.sslverifystatus", var)) {
+ curl_ssl_verify_status = git_config_bool(var, value);
+ return 0;
+ }
if (!strcmp("http.sslcipherlist", var))
return git_config_string(&ssl_cipherlist, var, value);
if (!strcmp("http.sslversion", var))
@@ -1133,6 +1138,11 @@ static CURL *get_curl_handle(void)
curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
}
+ if (curl_ssl_verify_status &&
+ curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
+ die(_("http.sslVerifyStatus is set, but the TLS backend of "
+ "this libcurl cannot verify certificate status"));
+
if (curl_http_version) {
long opt;
if (!get_curl_http_version_opt(curl_http_version, &opt)) {
diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
index 805bec025c..c11e96c1ac 100755
--- a/t/t5551-http-fetch-smart.sh
+++ b/t/t5551-http-fetch-smart.sh
@@ -680,6 +680,35 @@ test_expect_success 'passing hostname resolution information works' '
git -c "http.curloptResolve=$BOGUS_HOST:$LIB_HTTPD_PORT:127.0.0.1" ls-remote "$BOGUS_HTTPD_URL/smart/repo.git" >/dev/null
'
+test_lazy_prereq SSL_VERIFYSTATUS '
+ test "$HTTPD_PROTO" = "https" &&
+ test_might_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git" 2>err &&
+ ! grep "cannot verify certificate status" err
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
+ test_must_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
+ git -c http.sslVerifyStatus=false \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus applies to a matching URL' '
+ test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus is not applied to other URLs' '
+ git -c "http.https://example.com/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
# here user%40host is the URL-encoded version of user@host,
# which is our intentionally-odd username to catch parsing errors
url_user=$HTTPD_URL_USER/auth/smart/repo.git
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v4] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-17 18:52 ` [PATCH v4] " graysongordon-gl
@ 2026-08-17 19:19 ` Junio C Hamano
2026-08-18 7:50 ` Patrick Steinhardt
1 sibling, 0 replies; 16+ messages in thread
From: Junio C Hamano @ 2026-08-17 19:19 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git
graysongordon-gl <graysongordon1@gmail.com> writes:
> Verified: t5551 over plain http and t5559 over https both pass all 61
> tests, with the four new ones skipping on the former and running on the
> latter.
> Documentation/config/http.adoc | 17 +++++++++++++++++
> http.c | 10 ++++++++++
> t/t5551-http-fetch-smart.sh | 29 +++++++++++++++++++++++++++++
> 3 files changed, 56 insertions(+)
OK, instead of adding a new test script that weighs 72-line we are
testing the feature with 29-line addition, which sounds like a good
economy ;-).
The code changes and the documentation haven't changed since the
previous round, both looking good.
Will replace. Thanks.
> diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
> index 792a71b413..40b849bf7f 100644
> --- a/Documentation/config/http.adoc
> +++ b/Documentation/config/http.adoc
> @@ -196,6 +196,23 @@ http.sslVerify::
> over HTTPS. Defaults to true. Can be overridden by the
> `GIT_SSL_NO_VERIFY` environment variable.
>
> +http.sslVerifyStatus::
> + Whether to check the revocation status of the server
> + certificate using the stapled OCSP response supplied during
> + the TLS handshake ("OCSP stapling"). Defaults to false.
> ++
> +This is fail-closed: if the server staples no response, verification
> +fails. Set it per remote, e.g.
> +`http.https://example.com/.sslVerifyStatus`, rather than globally.
> ++
> +What it changes depends on the TLS backend libcurl was built against.
> +An OpenSSL-linked build ignores a stapled response unless this is set.
> +A GnuTLS-linked build consults the staple during ordinary certificate
> +verification, so it already rejects a revoked certificate under
> +`http.sslVerify` alone, and setting this to `false` does not disable
> +that. Where a backend cannot check the staple at all, git fails with an
> +error rather than continuing unchecked.
> +
> http.sslCert::
> File containing the SSL certificate when fetching or pushing
> over HTTPS. Can be overridden by the `GIT_SSL_CERT` environment
> diff --git a/http.c b/http.c
> index caccf2108e..94f8dd817a 100644
> --- a/http.c
> +++ b/http.c
> @@ -44,6 +44,7 @@ static CURL *curl_default;
> char curl_errorstr[CURL_ERROR_SIZE];
>
> static int curl_ssl_verify = -1;
> +static int curl_ssl_verify_status;
> static int curl_ssl_try;
> static char *curl_http_version;
> static char *ssl_cert;
> @@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
> curl_ssl_verify = git_config_bool(var, value);
> return 0;
> }
> + if (!strcmp("http.sslverifystatus", var)) {
> + curl_ssl_verify_status = git_config_bool(var, value);
> + return 0;
> + }
> if (!strcmp("http.sslcipherlist", var))
> return git_config_string(&ssl_cipherlist, var, value);
> if (!strcmp("http.sslversion", var))
> @@ -1133,6 +1138,11 @@ static CURL *get_curl_handle(void)
> curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
> }
>
> + if (curl_ssl_verify_status &&
> + curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
> + die(_("http.sslVerifyStatus is set, but the TLS backend of "
> + "this libcurl cannot verify certificate status"));
> +
> if (curl_http_version) {
> long opt;
> if (!get_curl_http_version_opt(curl_http_version, &opt)) {
> diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
> index 805bec025c..c11e96c1ac 100755
> --- a/t/t5551-http-fetch-smart.sh
> +++ b/t/t5551-http-fetch-smart.sh
> @@ -680,6 +680,35 @@ test_expect_success 'passing hostname resolution information works' '
> git -c "http.curloptResolve=$BOGUS_HOST:$LIB_HTTPD_PORT:127.0.0.1" ls-remote "$BOGUS_HTTPD_URL/smart/repo.git" >/dev/null
> '
>
> +test_lazy_prereq SSL_VERIFYSTATUS '
> + test "$HTTPD_PROTO" = "https" &&
> + test_might_fail git -c http.sslVerifyStatus=true \
> + ls-remote "$HTTPD_URL/smart/repo.git" 2>err &&
> + ! grep "cannot verify certificate status" err
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
> + test_must_fail git -c http.sslVerifyStatus=true \
> + ls-remote "$HTTPD_URL/smart/repo.git"
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
> + git -c http.sslVerifyStatus=false \
> + ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
> + test_line_count -gt 0 actual
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus applies to a matching URL' '
> + test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
> + ls-remote "$HTTPD_URL/smart/repo.git"
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus is not applied to other URLs' '
> + git -c "http.https://example.com/.sslVerifyStatus=true" \
> + ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
> + test_line_count -gt 0 actual
> +'
> +
> # here user%40host is the URL-encoded version of user@host,
> # which is our intentionally-odd username to catch parsing errors
> url_user=$HTTPD_URL_USER/auth/smart/repo.git
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v4] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-17 18:52 ` [PATCH v4] " graysongordon-gl
2026-08-17 19:19 ` Junio C Hamano
@ 2026-08-18 7:50 ` Patrick Steinhardt
2026-08-18 14:51 ` Grayson Gordon
2026-08-18 16:40 ` Junio C Hamano
1 sibling, 2 replies; 16+ messages in thread
From: Patrick Steinhardt @ 2026-08-18 7:50 UTC (permalink / raw)
To: graysongordon-gl; +Cc: gitster, git
On Mon, Aug 17, 2026 at 02:52:42PM -0400, graysongordon-gl wrote:
> From: Grayson Gordon <graysongordon1@gmail.com>
>
> git asks libcurl to verify the peer certificate and the hostname, but it
> never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
> TLS extension is never requested and any stapled OCSP response the server
> does send is ignored.
>
> On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
> response to the application and takes no view on it:
> SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
> whether the returned OCSP response(s) are acceptable or not", and libcurl
> only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
> will fetch from a server whose own staple says its certificate has been
> revoked.
>
> A GnuTLS-linked build behaves differently, and the difference does not
> come from curl. GnuTLS consults a stapled response inside
> gnutls_certificate_verify_peers(), so the failure surfaces through the
> verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
> not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
> server, therefore enforces revocation or not depending only on how its
> libcurl was built. That difference is documented here rather than papered
> over: this option turns the check on where the backend needs asking, and
> setting it to false does not turn the check off on GnuTLS.
This is only part of the story though: GnuTLS 3.8 introduced
GNUTLS_NO_STATUS_REQUEST, and curl 8.10 started to set that option in
case of `!verifystatus`. So with new-enough versions of both libraries,
Git behaves the same no matter whether we use OpenSSL or GnuTLS as
backend. See also aeb1a281ca (gtls: fix OCSP stapling management,
2024-08-20) in curl.
> Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
> Because http_options() is the collect_fn of a urlmatch config, the
> per-URL form works with no further changes:
>
> git config http.https://example.com/.sslVerifyStatus true
>
> It defaults to false, and has to. The option is fail-closed: libcurl fails
> verification when the server staples nothing at all, so turning this on
> globally would break every remote that does not staple.
>
> Leaving the default to libcurl is not an option either. The same
> complaint was raised there in https://github.com/curl/curl/issues/15483
> and closed as intentional ("Marked as enhancement since this was done on
> purpose"), with the observation that stapling is expected to see less use
> as Let's Encrypt drops OCSP support. If the check is to be reachable at
> all, the lever has to come from the application.
But... don't we still leave the default to libcurl? If
"http.sslVerifyStatus" is not set then we don't touch
`CURLOPT_SSL_VERIFYSTATUS`, either.
I might be misreading this though, as the whole commit message is quite
hard to digest. I'd assume that this is because it's generated by AI,
and it added a lot of the usual weird phrases to the message. It might
be a good idea to adapt the message to have a bit more of a human touch
to it.
> If the TLS backend cannot check the staple, curl_easy_setopt() returns
> CURLE_NOT_BUILT_IN. Fail loudly there rather than carrying on, since
> silently not checking is precisely what this option exists to prevent.
Makes sense.
> diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
> index 792a71b413..40b849bf7f 100644
> --- a/Documentation/config/http.adoc
> +++ b/Documentation/config/http.adoc
> @@ -196,6 +196,23 @@ http.sslVerify::
> over HTTPS. Defaults to true. Can be overridden by the
> `GIT_SSL_NO_VERIFY` environment variable.
>
> +http.sslVerifyStatus::
> + Whether to check the revocation status of the server
> + certificate using the stapled OCSP response supplied during
> + the TLS handshake ("OCSP stapling"). Defaults to false.
> ++
> +This is fail-closed: if the server staples no response, verification
> +fails. Set it per remote, e.g.
> +`http.https://example.com/.sslVerifyStatus`, rather than globally.
> ++
> +What it changes depends on the TLS backend libcurl was built against.
> +An OpenSSL-linked build ignores a stapled response unless this is set.
> +A GnuTLS-linked build consults the staple during ordinary certificate
> +verification, so it already rejects a revoked certificate under
> +`http.sslVerify` alone, and setting this to `false` does not disable
> +that. Where a backend cannot check the staple at all, git fails with an
> +error rather than continuing unchecked.
This information is not accurate because recent GnuTLS+libcurl versions
handle this the same as OpenSSL, as mentioned above.
Also, it might make sense to convert the backend-specific information
into a bulleted list as we may add more items to it going forward. Do we
have any info how other backends like mbedTLS behave? Or do we know that
those all fail.
> diff --git a/http.c b/http.c
> index caccf2108e..94f8dd817a 100644
> --- a/http.c
> +++ b/http.c
> @@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
> curl_ssl_verify = git_config_bool(var, value);
> return 0;
> }
> + if (!strcmp("http.sslverifystatus", var)) {
> + curl_ssl_verify_status = git_config_bool(var, value);
> + return 0;
> + }
> if (!strcmp("http.sslcipherlist", var))
> return git_config_string(&ssl_cipherlist, var, value);
> if (!strcmp("http.sslversion", var))
> @@ -1133,6 +1138,11 @@ static CURL *get_curl_handle(void)
> curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
> }
>
> + if (curl_ssl_verify_status &&
> + curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
> + die(_("http.sslVerifyStatus is set, but the TLS backend of "
> + "this libcurl cannot verify certificate status"));
Should we include the output of `curl_easy_strerror()` in the error
message? That'd cause us to include the following error message in case
we see CURLE_NOT_BUILT_IN:
case CURLE_NOT_BUILT_IN:
return "A requested feature, protocol or option was not found built-in in"
" this libcurl due to a build-time decision.";
So we could instead do:
if (curl_ssl_verify_status) {
CURLcode error = curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L);
if (error != CURLE_OK)
die(_("http.sslVerifyStatus is set, but could not enable OCSP status verification: %s"),
curl_easy_strerror(error));
}
> diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
> index 805bec025c..c11e96c1ac 100755
> --- a/t/t5551-http-fetch-smart.sh
> +++ b/t/t5551-http-fetch-smart.sh
> @@ -680,6 +680,35 @@ test_expect_success 'passing hostname resolution information works' '
> git -c "http.curloptResolve=$BOGUS_HOST:$LIB_HTTPD_PORT:127.0.0.1" ls-remote "$BOGUS_HTTPD_URL/smart/repo.git" >/dev/null
> '
>
> +test_lazy_prereq SSL_VERIFYSTATUS '
> + test "$HTTPD_PROTO" = "https" &&
> + test_might_fail git -c http.sslVerifyStatus=true \
> + ls-remote "$HTTPD_URL/smart/repo.git" 2>err &&
> + ! grep "cannot verify certificate status" err
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
> + test_must_fail git -c http.sslVerifyStatus=true \
> + ls-remote "$HTTPD_URL/smart/repo.git"
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
> + git -c http.sslVerifyStatus=false \
> + ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
> + test_line_count -gt 0 actual
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus applies to a matching URL' '
> + test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
> + ls-remote "$HTTPD_URL/smart/repo.git"
> +'
> +
> +test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus is not applied to other URLs' '
> + git -c "http.https://example.com/.sslVerifyStatus=true" \
> + ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
> + test_line_count -gt 0 actual
> +'
Can we reasonably add tests that send OCSP information and verify that
enabling "sslVerifyStatus" makes this work as expected?
Patrick
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v4] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-18 7:50 ` Patrick Steinhardt
@ 2026-08-18 14:51 ` Grayson Gordon
2026-08-18 16:40 ` Junio C Hamano
1 sibling, 0 replies; 16+ messages in thread
From: Grayson Gordon @ 2026-08-18 14:51 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: gitster, git
Patrick,
Thanks again for the feedback. I'm going to break this into sections
delimited by all caps headers to address each thing you mentioned.
ON GNUTLS VS OPENSSL DIFFERENCES
I appreciate you including the extra context around GnuTLS 3.8's
GNUTLS_NO_STATUS_REQUEST flag, and curl 8.10 setting it when
"verifystatus" is false.
Fair enough, said another way, if either of these are true:
Condition 1: curl is being built with GnuTLS version < 3.8. (There's
no NO_STATUS_REQUEST flag to set.)
OR
Condition 2: curl version < 8.10. (Curl's not using the flag.)
You'll see a discrepancy in cert verification behavior between the
versions of git built with GnuTLS vs OpenSSL.
While I appreciate the increased precision here, the purpose of my
contribution was to expose the functionality that enables git users to
set it if they so choose. As it stands today, this option is not
presented. So while the discrepancy is what incited me to look deeper,
it isn't the central reason I'm here.
You had a section further down that said: "This information is not
accurate because recent GnuTLS+libcurl versions
handle this the same as OpenSSL, as mentioned above." I think that
this response section suffices for both.
I didn't look into any other TLS backends, as curl's docs say that the
verifystatus option only works with GnuTLS and OpenSSL
https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYSTATUS.html and the other
backend options didn't apply to GitLab customers as it relates to our
CNG charts. There may still be value in looking and capturing it in
the git docs somewhere.
---------------
ON DEFAULT BEHAVIOR AND THE COMMIT MESSAGE
In reality we ARE leaving the default to curl without the flag being
set, so my comment "Leaving the default to libcurl is not an option
either." wasn't super precise either. A nit.
Again the change that's being introduced here is an OPTION for git
users to include the "verifystatus" flag.
Sorry that the commit message feels a bit awkward to you, I can rework
it to be a bit more direct and not spend as much time on
justifications if that'll be easier to read.
---------------
ON DESCRIPTIVE ERROR MESSAGES
Yes, I like this idea. We could give user's a much clearer error that
way. I'll include that. My tests grep for the current string though,
so I'll have to update that too.
---------------
ON COMPREHENSIVE TESTING
I'll leave this at you and Junio's discretion. I worked with him
earlier in this thread to avoid introducing another test file and keep
the testing succinct.
Right now these tests are just limited to parsing the config and
applying it to the user-provided remote.
We COULD do the full suite of tests that cover the full range of
cases/behaviors:
- The flag is set AND
- no staple sent (should fail)
- good staple (should pass)
- bad staple (should fail)
etc.
We're going to need a lot of infrax for that though:
- test CA.
- test server certificate issued by that CA.
- OCSP responder which knows the certificate's status.
- a way for the TLS server to obtain and staple that response.
- a way to control the response so you can test good vs revoked/invalid.
I set all of that stuff up in my own experiment repo, emulating this
with nginx in docker...
It's feasible, just need to know how you all would like it.
- Grayson
On Tue, Aug 18, 2026 at 3:50 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Mon, Aug 17, 2026 at 02:52:42PM -0400, graysongordon-gl wrote:
> > From: Grayson Gordon <graysongordon1@gmail.com>
> >
> > git asks libcurl to verify the peer certificate and the hostname, but it
> > never sets CURLOPT_SSL_VERIFYSTATUS, so the "Certificate Status Request"
> > TLS extension is never requested and any stapled OCSP response the server
> > does send is ignored.
> >
> > On an OpenSSL-linked build this is silent. OpenSSL hands the stapled
> > response to the application and takes no view on it:
> > SSL_CTX_set_tlsext_status_cb(3) says the callback "should determine
> > whether the returned OCSP response(s) are acceptable or not", and libcurl
> > only installs that callback when CURLOPT_SSL_VERIFYSTATUS is set. So git
> > will fetch from a server whose own staple says its certificate has been
> > revoked.
> >
> > A GnuTLS-linked build behaves differently, and the difference does not
> > come from curl. GnuTLS consults a stapled response inside
> > gnutls_certificate_verify_peers(), so the failure surfaces through the
> > verifypeer branch of curl's GnuTLS backend (lib/vtls/gtls.c) whether or
> > not CURLOPT_SSL_VERIFYSTATUS was ever set. The same git, against the same
> > server, therefore enforces revocation or not depending only on how its
> > libcurl was built. That difference is documented here rather than papered
> > over: this option turns the check on where the backend needs asking, and
> > setting it to false does not turn the check off on GnuTLS.
>
> This is only part of the story though: GnuTLS 3.8 introduced
> GNUTLS_NO_STATUS_REQUEST, and curl 8.10 started to set that option in
> case of `!verifystatus`. So with new-enough versions of both libraries,
> Git behaves the same no matter whether we use OpenSSL or GnuTLS as
> backend. See also aeb1a281ca (gtls: fix OCSP stapling management,
> 2024-08-20) in curl.
>
> > Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
> > Because http_options() is the collect_fn of a urlmatch config, the
> > per-URL form works with no further changes:
> >
> > git config http.https://example.com/.sslVerifyStatus true
> >
> > It defaults to false, and has to. The option is fail-closed: libcurl fails
> > verification when the server staples nothing at all, so turning this on
> > globally would break every remote that does not staple.
> >
> > Leaving the default to libcurl is not an option either. The same
> > complaint was raised there in https://github.com/curl/curl/issues/15483
> > and closed as intentional ("Marked as enhancement since this was done on
> > purpose"), with the observation that stapling is expected to see less use
> > as Let's Encrypt drops OCSP support. If the check is to be reachable at
> > all, the lever has to come from the application.
>
> But... don't we still leave the default to libcurl? If
> "http.sslVerifyStatus" is not set then we don't touch
> `CURLOPT_SSL_VERIFYSTATUS`, either.
>
> I might be misreading this though, as the whole commit message is quite
> hard to digest. I'd assume that this is because it's generated by AI,
> and it added a lot of the usual weird phrases to the message. It might
> be a good idea to adapt the message to have a bit more of a human touch
> to it.
>
> > If the TLS backend cannot check the staple, curl_easy_setopt() returns
> > CURLE_NOT_BUILT_IN. Fail loudly there rather than carrying on, since
> > silently not checking is precisely what this option exists to prevent.
>
> Makes sense.
>
> > diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
> > index 792a71b413..40b849bf7f 100644
> > --- a/Documentation/config/http.adoc
> > +++ b/Documentation/config/http.adoc
> > @@ -196,6 +196,23 @@ http.sslVerify::
> > over HTTPS. Defaults to true. Can be overridden by the
> > `GIT_SSL_NO_VERIFY` environment variable.
> >
> > +http.sslVerifyStatus::
> > + Whether to check the revocation status of the server
> > + certificate using the stapled OCSP response supplied during
> > + the TLS handshake ("OCSP stapling"). Defaults to false.
> > ++
> > +This is fail-closed: if the server staples no response, verification
> > +fails. Set it per remote, e.g.
> > +`http.https://example.com/.sslVerifyStatus`, rather than globally.
> > ++
> > +What it changes depends on the TLS backend libcurl was built against.
> > +An OpenSSL-linked build ignores a stapled response unless this is set.
> > +A GnuTLS-linked build consults the staple during ordinary certificate
> > +verification, so it already rejects a revoked certificate under
> > +`http.sslVerify` alone, and setting this to `false` does not disable
> > +that. Where a backend cannot check the staple at all, git fails with an
> > +error rather than continuing unchecked.
>
> This information is not accurate because recent GnuTLS+libcurl versions
> handle this the same as OpenSSL, as mentioned above.
>
> Also, it might make sense to convert the backend-specific information
> into a bulleted list as we may add more items to it going forward. Do we
> have any info how other backends like mbedTLS behave? Or do we know that
> those all fail.
>
> > diff --git a/http.c b/http.c
> > index caccf2108e..94f8dd817a 100644
> > --- a/http.c
> > +++ b/http.c
> > @@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
> > curl_ssl_verify = git_config_bool(var, value);
> > return 0;
> > }
> > + if (!strcmp("http.sslverifystatus", var)) {
> > + curl_ssl_verify_status = git_config_bool(var, value);
> > + return 0;
> > + }
> > if (!strcmp("http.sslcipherlist", var))
> > return git_config_string(&ssl_cipherlist, var, value);
> > if (!strcmp("http.sslversion", var))
> > @@ -1133,6 +1138,11 @@ static CURL *get_curl_handle(void)
> > curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
> > }
> >
> > + if (curl_ssl_verify_status &&
> > + curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L) != CURLE_OK)
> > + die(_("http.sslVerifyStatus is set, but the TLS backend of "
> > + "this libcurl cannot verify certificate status"));
>
> Should we include the output of `curl_easy_strerror()` in the error
> message? That'd cause us to include the following error message in case
> we see CURLE_NOT_BUILT_IN:
>
> case CURLE_NOT_BUILT_IN:
> return "A requested feature, protocol or option was not found built-in in"
> " this libcurl due to a build-time decision.";
>
> So we could instead do:
>
> if (curl_ssl_verify_status) {
> CURLcode error = curl_easy_setopt(result, CURLOPT_SSL_VERIFYSTATUS, 1L);
> if (error != CURLE_OK)
> die(_("http.sslVerifyStatus is set, but could not enable OCSP status verification: %s"),
> curl_easy_strerror(error));
> }
>
> > diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
> > index 805bec025c..c11e96c1ac 100755
> > --- a/t/t5551-http-fetch-smart.sh
> > +++ b/t/t5551-http-fetch-smart.sh
> > @@ -680,6 +680,35 @@ test_expect_success 'passing hostname resolution information works' '
> > git -c "http.curloptResolve=$BOGUS_HOST:$LIB_HTTPD_PORT:127.0.0.1" ls-remote "$BOGUS_HTTPD_URL/smart/repo.git" >/dev/null
> > '
> >
> > +test_lazy_prereq SSL_VERIFYSTATUS '
> > + test "$HTTPD_PROTO" = "https" &&
> > + test_might_fail git -c http.sslVerifyStatus=true \
> > + ls-remote "$HTTPD_URL/smart/repo.git" 2>err &&
> > + ! grep "cannot verify certificate status" err
> > +'
> > +
> > +test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
> > + test_must_fail git -c http.sslVerifyStatus=true \
> > + ls-remote "$HTTPD_URL/smart/repo.git"
> > +'
> > +
> > +test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
> > + git -c http.sslVerifyStatus=false \
> > + ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
> > + test_line_count -gt 0 actual
> > +'
> > +
> > +test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus applies to a matching URL' '
> > + test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
> > + ls-remote "$HTTPD_URL/smart/repo.git"
> > +'
> > +
> > +test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus is not applied to other URLs' '
> > + git -c "http.https://example.com/.sslVerifyStatus=true" \
> > + ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
> > + test_line_count -gt 0 actual
> > +'
>
> Can we reasonably add tests that send OCSP information and verify that
> enabling "sslVerifyStatus" makes this work as expected?
>
> Patrick
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH v4] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-18 7:50 ` Patrick Steinhardt
2026-08-18 14:51 ` Grayson Gordon
@ 2026-08-18 16:40 ` Junio C Hamano
1 sibling, 0 replies; 16+ messages in thread
From: Junio C Hamano @ 2026-08-18 16:40 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: graysongordon-gl, git
Patrick Steinhardt <ps@pks.im> writes:
> This is only part of the story though: GnuTLS 3.8 introduced
> GNUTLS_NO_STATUS_REQUEST, and curl 8.10 started to set that option in
> case of `!verifystatus`. So with new-enough versions of both libraries,
> Git behaves the same no matter whether we use OpenSSL or GnuTLS as
> backend. See also aeb1a281ca (gtls: fix OCSP stapling management,
> 2024-08-20) in curl.
Thanks for additional details.
>> Add an http.sslVerifyStatus boolean that sets CURLOPT_SSL_VERIFYSTATUS.
>> Because http_options() is the collect_fn of a urlmatch config, the
>> per-URL form works with no further changes:
>>
>> git config http.https://example.com/.sslVerifyStatus true
>>
>> It defaults to false, and has to. The option is fail-closed: libcurl fails
>> verification when the server staples nothing at all, so turning this on
>> globally would break every remote that does not staple.
>>
>> Leaving the default to libcurl is not an option either. The same
>> complaint was raised there in https://github.com/curl/curl/issues/15483
>> and closed as intentional ("Marked as enhancement since this was done on
>> purpose"), with the observation that stapling is expected to see less use
>> as Let's Encrypt drops OCSP support. If the check is to be reachable at
>> all, the lever has to come from the application.
>
> But... don't we still leave the default to libcurl? If
> "http.sslVerifyStatus" is not set then we don't touch
> `CURLOPT_SSL_VERIFYSTATUS`, either.
>
> I might be misreading this though, as the whole commit message is quite
> hard to digest. I'd assume that this is because it's generated by AI,
> and it added a lot of the usual weird phrases to the message. It might
> be a good idea to adapt the message to have a bit more of a human touch
> to it.
I too had trouble figuring out what the proposed log message really
wanted to say, but I wrote it off, blaming the difficulty on a
language barrier. But as you said, perhaps it is because it was
written by something that does not truly understand what it is
talking about. It may not have to explain things to readers as if
they were 5 years old, but it is definitely necessary to explain
well to readers as if they were humans with average intelligence
;-).
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v5] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-13 16:06 ` Junio C Hamano
2026-08-17 18:52 ` [PATCH v4] " graysongordon-gl
@ 2026-08-18 19:37 ` graysongordon-gl
2026-08-18 20:12 ` Junio C Hamano
1 sibling, 1 reply; 16+ messages in thread
From: graysongordon-gl @ 2026-08-18 19:37 UTC (permalink / raw)
To: git; +Cc: gitster, peff, avarab, ps, Grayson Gordon
From: Grayson Gordon <graysongordon1@gmail.com>
git never sets CURLOPT_SSL_VERIFYSTATUS, so libcurl never requests the
OCSP "Certificate Status Request" extension and any stapled response a
server sends is ignored, including responses that explicitly state the
certificate has been revoked.
Add an http.sslVerifyStatus boolean that maps to
CURLOPT_SSL_VERIFYSTATUS.
http_options() is already the collect_fn for a urlmatch config, so the
per-URL form works with no changes:
git config http.https://example.com/.sslVerifyStatus true
Defaults to false/"off". This is due to the nature of the OCSP protocol.
If enabled, git would expect to receive OCSP stapled responses. If the
stapled responses were not present, the connection would be blocked as
the status of the server's certificate could not be verified. This would
break connections to legitimate services that don't use OCSP as their
certificate revocation mechanism.
If the backend can't check the staple, curl_easy_setopt() returns
CURLE_NOT_BUILT_IN. Error message includes curl_easy_strerror() with
the option name to enable users to more easily identify a libcurl
built without status verification.
CURLOPT_SSL_VERIFYSTATUS has existed since libcurl 7.41.0, below our
7.61.0 floor, so no version guard is needed.
Tests are in t5551.
Signed-off-by: Grayson Gordon <graysongordon1@gmail.com>
---
Documentation/config/http.adoc | 9 +++++++++
http.c | 14 ++++++++++++++
t/t5551-http-fetch-smart.sh | 29 +++++++++++++++++++++++++++++
3 files changed, 52 insertions(+)
diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 792a71b413..6bc2e3823d 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -196,6 +196,15 @@ http.sslVerify::
over HTTPS. Defaults to true. Can be overridden by the
`GIT_SSL_NO_VERIFY` environment variable.
+http.sslVerifyStatus::
+ Whether to check the revocation status of the server
+ certificate using the stapled OCSP response supplied during
+ the TLS handshake ("OCSP stapling"). Defaults to false.
++
+This is fail-closed: if the server staples no response, verification
+fails. Set it per remote, e.g.
+`http.https://example.com/.sslVerifyStatus`, rather than globally.
+
http.sslCert::
File containing the SSL certificate when fetching or pushing
over HTTPS. Can be overridden by the `GIT_SSL_CERT` environment
diff --git a/http.c b/http.c
index caccf2108e..4a4dd40fe2 100644
--- a/http.c
+++ b/http.c
@@ -44,6 +44,7 @@ static CURL *curl_default;
char curl_errorstr[CURL_ERROR_SIZE];
static int curl_ssl_verify = -1;
+static int curl_ssl_verify_status;
static int curl_ssl_try;
static char *curl_http_version;
static char *ssl_cert;
@@ -400,6 +401,10 @@ static int http_options(const char *var, const char *value,
curl_ssl_verify = git_config_bool(var, value);
return 0;
}
+ if (!strcmp("http.sslverifystatus", var)) {
+ curl_ssl_verify_status = git_config_bool(var, value);
+ return 0;
+ }
if (!strcmp("http.sslcipherlist", var))
return git_config_string(&ssl_cipherlist, var, value);
if (!strcmp("http.sslversion", var))
@@ -1133,6 +1138,15 @@ static CURL *get_curl_handle(void)
curl_easy_setopt(result, CURLOPT_SSL_VERIFYHOST, 2L);
}
+ if (curl_ssl_verify_status) {
+ CURLcode ret = curl_easy_setopt(result,
+ CURLOPT_SSL_VERIFYSTATUS, 1L);
+ if (ret != CURLE_OK)
+ die(_("http.sslVerifyStatus is set, but could not "
+ "enable OCSP status verification: %s"),
+ curl_easy_strerror(ret));
+ }
+
if (curl_http_version) {
long opt;
if (!get_curl_http_version_opt(curl_http_version, &opt)) {
diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
index 805bec025c..75ab07f031 100755
--- a/t/t5551-http-fetch-smart.sh
+++ b/t/t5551-http-fetch-smart.sh
@@ -680,6 +680,35 @@ test_expect_success 'passing hostname resolution information works' '
git -c "http.curloptResolve=$BOGUS_HOST:$LIB_HTTPD_PORT:127.0.0.1" ls-remote "$BOGUS_HTTPD_URL/smart/repo.git" >/dev/null
'
+test_lazy_prereq SSL_VERIFYSTATUS '
+ test "$HTTPD_PROTO" = "https" &&
+ test_might_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git" 2>err &&
+ ! grep "http.sslVerifyStatus is set" err
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=true fails without a staple' '
+ test_must_fail git -c http.sslVerifyStatus=true \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'http.sslVerifyStatus=false is a no-op' '
+ git -c http.sslVerifyStatus=false \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus applies to a matching URL' '
+ test_must_fail git -c "http.$HTTPD_URL/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git"
+'
+
+test_expect_success SSL_VERIFYSTATUS 'per-URL sslVerifyStatus is not applied to other URLs' '
+ git -c "http.https://example.com/.sslVerifyStatus=true" \
+ ls-remote "$HTTPD_URL/smart/repo.git" >actual &&
+ test_line_count -gt 0 actual
+'
+
# here user%40host is the URL-encoded version of user@host,
# which is our intentionally-odd username to catch parsing errors
url_user=$HTTPD_URL_USER/auth/smart/repo.git
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH v5] http: add http.sslVerifyStatus to check stapled OCSP responses
2026-08-18 19:37 ` [PATCH v5] " graysongordon-gl
@ 2026-08-18 20:12 ` Junio C Hamano
0 siblings, 0 replies; 16+ messages in thread
From: Junio C Hamano @ 2026-08-18 20:12 UTC (permalink / raw)
To: graysongordon-gl; +Cc: git, peff, avarab, ps
graysongordon-gl <graysongordon1@gmail.com> writes:
> +http.sslVerifyStatus::
> + Whether to check the revocation status of the server
> + certificate using the stapled OCSP response supplied during
> + the TLS handshake ("OCSP stapling"). Defaults to false.
> ++
> +This is fail-closed: if the server staples no response, verification
> +fails. Set it per remote, e.g.
> +`http.https://example.com/.sslVerifyStatus`, rather than globally.
I do not see us describe a knob or setting that can stop the
operation depending on some condition as "fail-closed". Can we
rephrase this for regular human beings? Perhaps
Whether to refuse connecting to the server when its
certificate has been revoked. Default to false, allowing
connection even when its certificate is not known to be
still valid.
or something like that might be a good starting point. After all,
the "check revocation and/or validity" is *not* the primary
objective from the end-user's point of view. Ensuring that they do
not talk to suspicious servers is.
Thanks.
^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-08-18 20:12 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-11 17:02 [PATCH] http: add http.sslVerifyStatus to check stapled OCSP responses graysongordon-gl
2026-08-11 19:28 ` Junio C Hamano
2026-08-11 20:44 ` [PATCH v2] " graysongordon-gl
2026-08-12 6:25 ` Patrick Steinhardt
2026-08-12 15:53 ` Grayson Gordon
2026-08-12 14:17 ` Junio C Hamano
2026-08-12 18:25 ` [PATCH v3] " graysongordon-gl
2026-08-12 21:34 ` Junio C Hamano
2026-08-13 16:06 ` Junio C Hamano
2026-08-17 18:52 ` [PATCH v4] " graysongordon-gl
2026-08-17 19:19 ` Junio C Hamano
2026-08-18 7:50 ` Patrick Steinhardt
2026-08-18 14:51 ` Grayson Gordon
2026-08-18 16:40 ` Junio C Hamano
2026-08-18 19:37 ` [PATCH v5] " graysongordon-gl
2026-08-18 20:12 ` Junio C Hamano
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.