diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc index 792a71b413..b54f627969 100644 --- a/Documentation/config/http.adoc +++ b/Documentation/config/http.adoc @@ -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 diff --git a/http.c b/http.c index 510204e45f..240e7620d8 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/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 @@ -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 diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf index 0627ef1433..f78f9cc642 100644 --- a/t/lib-httpd/apache.conf +++ b/t/lib-httpd/apache.conf @@ -243,6 +243,22 @@ SSLSessionCache none SSLEngine On + + + LoadModule socache_shmcb_module modules/mod_socache_shmcb.so + + +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 + + 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 f65eb04684..79d105d09e 100644 --- a/t/meson.build +++ b/t/meson.build @@ -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', 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" +' + +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" "$@" + ) +} + +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