Re: [PATCH v7] http: add http.sslVerifyStatus to check stapled OCSP responses
- From
SZEDER Gábor <szeder.dev@gmail.com>
- Date
- Sep 23, 2026, 21:07 UTC
- Message-ID
- <arQ/nOH+o3XwQFD/@szeder.dev>
- In-Reply-To
- <20260915162348.97792-1-ggordon@gitlab.com>
On Tue, Sep 15, 2026 at 12:23:48PM -0400, graysongordon-gl wrote:
Show 37 quoted lines
> 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. The error message includes curl_easy_strerror() > along with the option name, so a libcurl built without status > verification is easy to identify. > > CURLOPT_SSL_VERIFYSTATUS has existed since libcurl 7.41.0, below our > 7.61.0 floor, so no version guard is needed. > > The tests that need no OCSP infrastructure stay in t5551, which t5559 > runs over https. The rest need a certificate authority, a responder to > answer for it and a server configured to staple, so lib-httpd gains an > opt-in LIB_HTTPD_OCSP mode and t5585 uses it to check that a "good" > staple is accepted, a "revoked" one is refused, and that the revoked one > is ignored when the option is off. > > Signed-off-by: Grayson Gordon <graysongordon1@gmail.com> > ---
This patch was merged to 'next' the other day, and the last test in the new t5585 fails on my system.
Show 67 quoted lines
> diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
> index 115455784c..554b0e44fa 100644
> --- a/t/lib-httpd.sh
> +++ b/t/lib-httpd.sh
> @@ -25,6 +25,7 @@
> # LIB_HTTPD_DAV enable DAV
> # LIB_HTTPD_SVN enable SVN at given location (e.g. "svn")
> # LIB_HTTPD_SSL enable SSL
> +# LIB_HTTPD_OCSP enable OCSP stapling
> # LIB_HTTPD_PROXY enable proxy
> #
> # Copyright (c) 2008 Clemens Buchacher <drizzd@aon.at>
> @@ -183,15 +184,26 @@ prepare_httpd() {
>
> ln -s "$LIB_HTTPD_MODULE_PATH" "$HTTPD_ROOT_PATH/modules"
>
> + if test -n "$LIB_HTTPD_OCSP"
> + then
> + LIB_HTTPD_SSL=t
> + fi
> +
> if test -n "$LIB_HTTPD_SSL"
> then
> HTTPD_PROTO=https
>
> - RANDFILE_PATH="$HTTPD_ROOT_PATH"/.rnd openssl req \
> - -config "$TEST_PATH/ssl.cnf" \
> - -new -x509 -nodes \
> - -out "$HTTPD_ROOT_PATH/httpd.pem" \
> - -keyout "$HTTPD_ROOT_PATH/httpd.pem"
> + if test -n "$LIB_HTTPD_OCSP"
> + then
> + prepare_ocsp_stapling
> + HTTPD_PARA="$HTTPD_PARA -DOCSP"
> + else
> + RANDFILE_PATH="$HTTPD_ROOT_PATH"/.rnd openssl req \
> + -config "$TEST_PATH/ssl.cnf" \
> + -new -x509 -nodes \
> + -out "$HTTPD_ROOT_PATH/httpd.pem" \
> + -keyout "$HTTPD_ROOT_PATH/httpd.pem"
> + fi
> GIT_SSL_NO_VERIFY=t
> export GIT_SSL_NO_VERIFY
> HTTPD_PARA="$HTTPD_PARA -DSSL"
> @@ -262,6 +274,114 @@ stop_httpd() {
> -f "$TEST_PATH/apache.conf" $HTTPD_PARA -k stop
> }
>
> +restart_httpd () {
> + httpd_pid=$(cat "$HTTPD_ROOT_PATH/httpd.pid") &&
> + stop_httpd &&
> + while kill -0 "$httpd_pid" 2>/dev/null
> + do
> + sleep 1
> + done &&
> + "$LIB_HTTPD_PATH" -d "$HTTPD_ROOT_PATH" \
> + -f "$TEST_PATH/apache.conf" $HTTPD_PARA \
> + -c "Listen 127.0.0.1:$LIB_HTTPD_PORT" -k start
> +}
> +
> +# Check if the linked libcurl can verify stapled OCSP responses.
> +test_lazy_prereq SSL_VERIFYSTATUS '
> + test "$HTTPD_PROTO" = "https" &&
> + test_might_fail git -c http.sslVerifyStatus=true \
> + ls-remote "$HTTPD_URL" 2>err &&
> + ! grep "http.sslVerifyStatus is set" err
> +'When checking this prereq in t5585, I get the following trace:
mkdir -p "$TRASH_DIRECTORY/prereq-test-dir-SSL_VERIFYSTATUS" && ( cd "$TRASH_DIRECTORY/prereq-test-dir-SSL_VERIFYSTATUS" && test "$HTTPD_PROTO" = "https" && test_might_fail git -c http.sslVerifyStatus=true \ ls-remote "$HTTPD_URL" 2>err && cat err && # debug ! grep "http.sslVerifyStatus is set" err ) + mkdir -p /home/szeder/src/git/t/trash directory.t5585-http-ssl-ocsp/prereq-test-dir-SSL_VERIFYSTATUS + cd /home/szeder/src/git/t/trash directory.t5585-http-ssl-ocsp/prereq-test-dir-SSL_VERIFYSTATUS + test https = https + test_might_fail git -c http.sslVerifyStatus=true ls-remote https://127.0.0.1:5585 + cat err fatal: repository 'https://127.0.0.1:5585/' not found + grep http.sslVerifyStatus is set err prerequisite SSL_VERIFYSTATUS ok
I added that 'cat err' to see the error message. Turns out that 'git ls-remote' can't even find the repository on the remote, but the prereq is still considered fulfilled. Is that right?
In t5559 I get the following trace:
+ mkdir -p /home/szeder/src/git/t/trash directory.t5559-http-fetch-smart-http2/prereq-test-dir-SSL_VERIFYSTATUS + cd /home/szeder/src/git/t/trash directory.t5559-http-fetch-smart-http2/prereq-test-dir-SSL_VERIFYSTATUS + test https = https + test_might_fail git -c http.sslVerifyStatus=true ls-remote https://127.0.0.1:5559 + cat err fatal: unable to access 'https://127.0.0.1:5559/': No OCSP response received + grep http.sslVerifyStatus is set err prerequisite SSL_VERIFYSTATUS ok
This time the error message talks about missing OCSP response, but the prereq is still considered fulfilled. Again: is that right?!
Instead of the lack of a certain string in the error message, is there something positive that we can test instead?
Show 182 quoted lines
> +# Set up a certificate authority. It issues certificate "httpd.pem"
> +# and is able to revoke it. Used instead of the self-signed
> +# certificate when LIB_HTTPD_OCSP is set.
> +prepare_ocsp_stapling () {
> + LIB_HTTPD_OCSP_PORT=$((LIB_HTTPD_PORT + 10000))
> +
> + # Referenced by ocsp-ca.cnf.
> + OCSP_CA_DIR="$HTTPD_ROOT_PATH/ocsp-ca"
> + OCSP_URI="http://127.0.0.1:$LIB_HTTPD_OCSP_PORT"
> + export OCSP_CA_DIR OCSP_URI
> +
> + mkdir -p "$OCSP_CA_DIR/newcerts" &&
> + >"$OCSP_CA_DIR/index.txt" &&
> + echo 1000 >"$OCSP_CA_DIR/serial" &&
> +
> + openssl req -config "$TEST_PATH/ocsp-ca.cnf" \
> + -new -x509 -nodes -days 2 \
> + -subj "/CN=git-test-ca" -extensions v3_ca \
> + -keyout "$HTTPD_ROOT_PATH/ca.key" \
> + -out "$HTTPD_ROOT_PATH/ca.pem" &&
> + openssl req -config "$TEST_PATH/ocsp-ca.cnf" \
> + -new -nodes \
> + -subj "/CN=127.0.0.1" \
> + -keyout "$HTTPD_ROOT_PATH/httpd.key" \
> + -out "$HTTPD_ROOT_PATH/httpd.csr" &&
> + openssl ca -config "$TEST_PATH/ocsp-ca.cnf" -batch \
> + -cert "$HTTPD_ROOT_PATH/ca.pem" \
> + -keyfile "$HTTPD_ROOT_PATH/ca.key" \
> + -in "$HTTPD_ROOT_PATH/httpd.csr" \
> + -out "$HTTPD_ROOT_PATH/httpd.crt" &&
> + cat "$HTTPD_ROOT_PATH/httpd.key" "$HTTPD_ROOT_PATH/httpd.crt" \
> + >"$HTTPD_ROOT_PATH/httpd.pem"
> +}
> +
> +run_ocsp_responder () {
> + openssl ocsp -port "$LIB_HTTPD_OCSP_PORT" \
> + -index "$OCSP_CA_DIR/index.txt" \
> + -CA "$HTTPD_ROOT_PATH/ca.pem" \
> + -rsigner "$HTTPD_ROOT_PATH/ca.pem" \
> + -rkey "$HTTPD_ROOT_PATH/ca.key" \
> + -nmin 60 >>"$HTTPD_ROOT_PATH/ocsp.log" 2>&1 &
> + echo $! >"$HTTPD_ROOT_PATH/ocsp.pid"
> +
> + for i in $(test_seq 1 10)
> + do
> + if openssl ocsp -no_nonce \
> + -CAfile "$HTTPD_ROOT_PATH/ca.pem" \
> + -issuer "$HTTPD_ROOT_PATH/ca.pem" \
> + -cert "$HTTPD_ROOT_PATH/httpd.crt" \
> + -url "$OCSP_URI" >/dev/null 2>&1
> + then
> + return 0
> + fi
> + sleep 1
> + done
> + return 1
> +}
> +
> +start_ocsp_responder () {
> + test_atexit stop_ocsp_responder
> +
> + if ! run_ocsp_responder
> + then
> + cat "$HTTPD_ROOT_PATH"/ocsp.log >&4 2>/dev/null
> + test_skip_or_die GIT_TEST_HTTPD "OCSP responder setup failed"
> + fi
> +}
> +
> +stop_ocsp_responder () {
> + if test -f "$HTTPD_ROOT_PATH/ocsp.pid"
> + then
> + kill "$(cat "$HTTPD_ROOT_PATH/ocsp.pid")" 2>/dev/null
> + rm -f "$HTTPD_ROOT_PATH/ocsp.pid"
> + fi
> +}
> +
> +# Revoke the certificate used by httpd and make both the OCSP responder
> +# and httpd aware of it.
> +revoke_httpd_cert () {
> + openssl ca -config "$TEST_PATH/ocsp-ca.cnf" \
> + -cert "$HTTPD_ROOT_PATH/ca.pem" \
> + -keyfile "$HTTPD_ROOT_PATH/ca.key" \
> + -revoke "$HTTPD_ROOT_PATH/httpd.crt" &&
> + stop_ocsp_responder &&
> + run_ocsp_responder &&
> + restart_httpd
> +}
> +
> test_http_push_nonff () {
> REMOTE_REPO=$1
> LOCAL_REPO=$2
> diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf
> index 4149fc1078..de5ca45bb8 100644
> --- a/t/lib-httpd/apache.conf
> +++ b/t/lib-httpd/apache.conf
> @@ -242,6 +242,22 @@ SSLSessionCache none
> SSLEngine On
> </IfDefine>
>
> +<IfDefine OCSP>
> +<IfModule !mod_socache_shmcb.c>
> + LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
> +</IfModule>
> +
> +SSLCertificateChainFile ca.pem
> +SSLUseStapling On
> +# Stapling needs a mutex, which apache would put in a system-wide
> +# runtime directory that need not be writable. Keep it in the server
> +# root, or httpd refuses to start instead of skipping the tests.
> +DefaultRuntimeDir .
> +SSLStaplingCache shmcb:ssl_stapling(65536)
> +# Staple non-"good" responses too, so clients get to see "revoked".
> +SSLStaplingReturnResponderErrors On
> +</IfDefine>
> +
> <Location /auth/>
> AuthType Basic
> AuthName "git-auth"
> diff --git a/t/lib-httpd/ocsp-ca.cnf b/t/lib-httpd/ocsp-ca.cnf
> new file mode 100644
> index 0000000000..47a58139b5
> --- /dev/null
> +++ b/t/lib-httpd/ocsp-ca.cnf
> @@ -0,0 +1,35 @@
> +[ ca ]
> +default_ca = CA_default
> +
> +[ CA_default ]
> +dir = $ENV::OCSP_CA_DIR
> +database = $dir/index.txt
> +new_certs_dir = $dir/newcerts
> +serial = $dir/serial
> +default_md = sha256
> +default_days = 2
> +policy = policy_anything
> +email_in_dn = no
> +unique_subject = no
> +x509_extensions = server_cert
> +
> +[ policy_anything ]
> +commonName = supplied
> +
> +[ req ]
> +default_bits = 2048
> +distinguished_name = req_distinguished_name
> +prompt = no
> +
> +[ req_distinguished_name ]
> +# The subject is always given on the command line via -subj.
> +
> +[ v3_ca ]
> +basicConstraints = critical, CA:TRUE
> +keyUsage = critical, digitalSignature, keyCertSign, cRLSign
> +subjectKeyIdentifier = hash
> +
> +[ server_cert ]
> +basicConstraints = CA:FALSE
> +subjectAltName = IP:127.0.0.1
> +authorityInfoAccess = OCSP;URI:$ENV::OCSP_URI
> diff --git a/t/meson.build b/t/meson.build
> index 3ca7b27104..72cbd12d8f 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -728,6 +728,7 @@ integration_tests = [
> 't5582-fetch-negative-refspec.sh',
> 't5583-push-branches.sh',
> 't5584-http-429-retry.sh',
> + 't5585-http-ssl-ocsp.sh',
> 't5600-clone-fail-cleanup.sh',
> 't5601-clone.sh',
> 't5602-clone-remote-exec.sh',
> diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
> index 805bec025c..c51b14291d 100755
> --- a/t/t5551-http-fetch-smart.sh
> +++ b/t/t5551-http-fetch-smart.sh
> @@ -680,6 +680,28 @@ 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_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"Shouldn't we check the error message, to make sure that the command failed for the expected reason (here and in t5585 as well)?
Show 55 quoted lines
> +'
> +
> +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
> diff --git a/t/t5585-http-ssl-ocsp.sh b/t/t5585-http-ssl-ocsp.sh
> new file mode 100755
> index 0000000000..0d1310215f
> --- /dev/null
> +++ b/t/t5585-http-ssl-ocsp.sh
> @@ -0,0 +1,55 @@
> +#!/bin/sh
> +
> +test_description='verification of stapled OCSP responses via http.sslVerifyStatus'
> +
> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
> +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
> +
> +. ./test-lib.sh
> +
> +LIB_HTTPD_OCSP=1
> +. "$TEST_DIRECTORY"/lib-httpd.sh
> +
> +start_httpd
> +start_ocsp_responder
> +
> +test_expect_success 'setup repository' '
> + test_commit one &&
> + git init --bare "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
> + git push "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" HEAD:refs/heads/main
> +'
> +
> +# lib-httpd.sh exports GIT_SSL_NO_VERIFY, which would keep us from ever
> +# looking at the certificate. Trust our own CA instead.
> +with_ssl_verification () {
> + (
> + sane_unset GIT_SSL_NO_VERIFY &&
> + GIT_SSL_CAINFO="$HTTPD_ROOT_PATH/ca.pem" "$@"According to our CodingGuidelines, a temporary variable assignment like this should not be used for shell functions for portability reasons. In most test cases this is fine, becase "$@" is a git command, but ...
Show 20 quoted lines
> + ) > +} > + > +test_expect_success SSL_VERIFYSTATUS 'certificate verification works against test CA' ' > + with_ssl_verification git ls-remote "$HTTPD_URL/smart/repo.git" >actual && > + test_line_count -gt 0 actual > +' > + > +test_expect_success SSL_VERIFYSTATUS 'fetch succeeds with stapled "good" OCSP response' ' > + with_ssl_verification git -c http.sslVerifyStatus=true \ > + ls-remote "$HTTPD_URL/smart/repo.git" >actual && > + test_line_count -gt 0 actual > +' > + > +test_expect_success SSL_VERIFYSTATUS 'revoked certificate is rejected' ' > + revoke_httpd_cert && > + with_ssl_verification test_must_fail git -c http.sslVerifyStatus=true \ > + ls-remote "$HTTPD_URL/smart/repo.git" 2>err && > + test_grep -i -e "ocsp" -e "revocation" -e "revoked" -e "certificate status" err > +'
... in this case "$@" is the test_must_fail shell function.
Please set and then export that variable instead; it's already in a subshell because of the sane_unset anyway.
Show 5 quoted lines
> +# Depends on the certificate revoked by the preceding test. > +test_expect_success SSL_VERIFYSTATUS 'revoked certificate is accepted without http.sslVerifyStatus' ' > + with_ssl_verification git ls-remote "$HTTPD_URL/smart/repo.git" >actual && > + test_line_count -gt 0 actual > +'
So this test case fails for me with the following trace output:
expecting success of 5585.5 'revoked certificate is accepted without http.sslVerifyStatus': with_ssl_verification git ls-remote "$HTTPD_URL/smart/repo.git" >actual && test_line_count -gt 0 actual + with_ssl_verification git ls-remote https://127.0.0.1:5585/smart/repo.git + sane_unset GIT_SSL_NO_VERIFY + unset GIT_SSL_NO_VERIFY + return 0 + GIT_SSL_CAINFO=/home/szeder/src/git/t/trash directory.t5585-http-ssl-ocsp/httpd/ca.pem git ls-remote https://127.0.0.1:5585/smart/repo.git fatal: unable to access 'https://127.0.0.1:5585/smart/repo.git/': server certificate verification failed. CAfile: /home/szeder/src/git/t/trash directory.t5585-http-ssl-ocsp/httpd/ca.pem CRLfile: none error: last command exited with $?=128 not ok 5 - revoked certificate is accepted without http.sslVerifyStatus # # with_ssl_verification git ls-remote "$HTTPD_URL/smart/repo.git" >actual && # test_line_count -gt 0 actual # libcurl is 7.81.0, apache is 2.4.52 (whatever is shipped in this slowly aging LTS...)