Merge branch 'gg/http-ssl-verify-status' into seen

The HTTP transport has been taught to check the revocation status of
the server certificate using the stapled OCSP response during the
TLS handshake via a new 'http.sslVerifyStatus' configuration
variable.

* gg/http-ssl-verify-status:
  http: add http.sslVerifyStatus to check stapled OCSP responses
Junio C Hamano 2026-09-17 12:54:39 -07:00
commit 2c47028e63
8 changed files with 282 additions and 5 deletions

View File

@ -196,6 +196,20 @@ 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, which
allows connections to servers without validating if the
certificate has been revoked by the certificate authority.
Enabling this option will prevent connections to servers that
have a certificate status other than "good" per RFC 6960.
Connections to servers that do not return a stapled response
will also be refused.
+
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

14
http.c
View File

@ -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)) {

View File

@ -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
'

# 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

View File

@ -243,6 +243,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"

35
t/lib-httpd/ocsp-ca.cnf Normal file
View File

@ -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

View File

@ -730,6 +730,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',

View File

@ -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"
'

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

55
t/t5585-http-ssl-ocsp.sh Executable file
View File

@ -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" "$@"
)
}

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
'

# 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
'

test_done