From f68f542eb63efb0f50e00d2376196f5bd48ed41e Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 6 Mar 2023 15:19:29 -0800 Subject: [PATCH 1/4] http: introduce schannel streams for windows Provide a stream interface for Schannel - the native crypto APIs - on Windows. This allows Windows to use the same HTTP transport that all the other platforms use, with its own native crypto. Ultimately this allows us to deprecate WinHTTP and we need not add support for our socket changes in two places (our HTTP stack and the WinHTTP stack). --- .github/workflows/main.yml | 12 +- .github/workflows/nightly.yml | 36 +- CMakeLists.txt | 6 - cmake/SelectHTTPSBackend.cmake | 23 +- cmake/SelectHashes.cmake | 4 + cmake/SelectWinHTTP.cmake | 17 - src/CMakeLists.txt | 1 - src/libgit2/libgit2.c | 2 + src/libgit2/streams/schannel.c | 715 ++++++++++++++++++++++++++++ src/libgit2/streams/schannel.h | 28 ++ src/libgit2/streams/socket.c | 88 ++-- src/libgit2/streams/socket.h | 2 + src/libgit2/streams/tls.c | 5 + src/util/git2_features.h.in | 1 + tests/libgit2/online/clone.c | 13 +- tests/libgit2/stream/registration.c | 6 +- 16 files changed, 879 insertions(+), 80 deletions(-) delete mode 100644 cmake/SelectWinHTTP.cmake create mode 100644 src/libgit2/streams/schannel.c create mode 100644 src/libgit2/streams/schannel.h diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0eedab87a20..cdcea1644a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -130,19 +130,19 @@ jobs: SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true setup-script: osx - - name: "Windows (amd64, Visual Studio)" + - name: "Windows (amd64, Visual Studio, Schannel)" id: windows-amd64-vs os: windows-2019 setup-script: win32 env: ARCH: amd64 CMAKE_GENERATOR: Visual Studio 16 2019 - CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_SSH=ON -DCMAKE_PREFIX_PATH=D:\Temp\libssh2 + CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_HTTPS=Schannel -DUSE_SSH=ON -DCMAKE_PREFIX_PATH=D:\Temp\libssh2 BUILD_PATH: C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\CMake\bin;D:\Temp\libssh2\bin BUILD_TEMP: D:\Temp SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (x86, Visual Studio)" + - name: "Windows (x86, Visual Studio, WinHTTP)" id: windows-x86-vs os: windows-2019 setup-script: win32 @@ -154,7 +154,7 @@ jobs: BUILD_TEMP: D:\Temp SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (amd64, mingw)" + - name: "Windows (amd64, mingw, WinHTTP)" id: windows-amd64-mingw os: windows-2019 setup-script: mingw @@ -166,14 +166,14 @@ jobs: BUILD_PATH: D:\Temp\mingw64\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\CMake\bin SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (x86, mingw)" + - name: "Windows (x86, mingw, Schannel)" id: windows-x86-mingw os: windows-2019 setup-script: mingw env: ARCH: x86 CMAKE_GENERATOR: MinGW Makefiles - CMAKE_OPTIONS: -DDEPRECATE_HARD=ON + CMAKE_OPTIONS: -DDEPRECATE_HARD=ON -DUSE_HTTPS=Schannel BUILD_TEMP: D:\Temp BUILD_PATH: D:\Temp\mingw32\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\CMake\bin SKIP_SSH_TESTS: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5a0b7d12b0b..f461530ae71 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -162,32 +162,39 @@ jobs: SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true setup-script: osx - - name: "Windows (amd64, Visual Studio)" + - name: "Windows (amd64, Visual Studio, WinHTTP)" os: windows-2019 env: ARCH: amd64 CMAKE_GENERATOR: Visual Studio 16 2019 - CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON + CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_HTTPS=WinHTTP SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (no mmap)" + - name: "Windows (x86, Visual Studio, WinHTTP)" + os: windows-2019 + env: + ARCH: x86 + CMAKE_GENERATOR: Visual Studio 16 2019 + CMAKE_OPTIONS: -A Win32 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_HTTPS=WinHTTP -DUSE_SHA1=HTTPS -DUSE_BUNDLED_ZLIB=ON + SKIP_SSH_TESTS: true + SKIP_NEGOTIATE_TESTS: true + - name: "Windows (amd64, Visual Studio, Schannel)" os: windows-2019 env: ARCH: amd64 CMAKE_GENERATOR: Visual Studio 16 2019 - CFLAGS: -DNO_MMAP - CMAKE_OPTIONS: -A x64 -DDEPRECATE_HARD=ON + CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_HTTPS=Schannel SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (x86, Visual Studio)" + - name: "Windows (x86, Visual Studio, Schannel)" os: windows-2019 env: ARCH: x86 CMAKE_GENERATOR: Visual Studio 16 2019 - CMAKE_OPTIONS: -A Win32 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_SHA1=HTTPS -DUSE_BUNDLED_ZLIB=ON + CMAKE_OPTIONS: -A Win32 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DUSE_HTTPS=Schannel -DUSE_BUNDLED_ZLIB=ON SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (amd64, mingw)" + - name: "Windows (amd64, mingw, WinHTTP)" os: windows-2019 setup-script: mingw env: @@ -198,17 +205,26 @@ jobs: BUILD_PATH: D:\Temp\mingw64\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\CMake\bin SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true - - name: "Windows (x86, mingw)" + - name: "Windows (x86, mingw, Schannel)" os: windows-2019 setup-script: mingw env: ARCH: x86 CMAKE_GENERATOR: MinGW Makefiles - CMAKE_OPTIONS: -DDEPRECATE_HARD=ON + CMAKE_OPTIONS: -DDEPRECATE_HARD=ON -DUSE_HTTPS=Schannel BUILD_TEMP: D:\Temp BUILD_PATH: D:\Temp\mingw32\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\CMake\bin SKIP_SSH_TESTS: true SKIP_NEGOTIATE_TESTS: true + - name: "Windows (no mmap)" + os: windows-2019 + env: + ARCH: amd64 + CMAKE_GENERATOR: Visual Studio 16 2019 + CFLAGS: -DNO_MMAP + CMAKE_OPTIONS: -A x64 -DDEPRECATE_HARD=ON + SKIP_SSH_TESTS: true + SKIP_NEGOTIATE_TESTS: true - name: "Linux (Bionic, GCC, dynamically-loaded OpenSSL)" container: name: bionic diff --git a/CMakeLists.txt b/CMakeLists.txt index 710915da1bd..7aed74df67e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,12 +82,6 @@ if(MSVC) option(WIN32_LEAKCHECK "Enable leak reporting via crtdbg" OFF) endif() -if(WIN32) - # By default, libgit2 is built with WinHTTP. To use the built-in - # HTTP transport, invoke CMake with the "-DUSE_WINHTTP=OFF" argument. - option(USE_WINHTTP "Use Win32 WinHTTP routines" ON) -endif() - if(NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) endif() diff --git a/cmake/SelectHTTPSBackend.cmake b/cmake/SelectHTTPSBackend.cmake index 20221bf9f64..64c7a1097ce 100644 --- a/cmake/SelectHTTPSBackend.cmake +++ b/cmake/SelectHTTPSBackend.cmake @@ -19,7 +19,7 @@ if(USE_HTTPS) message(STATUS "Security framework is too old, falling back to OpenSSL") set(USE_HTTPS "OpenSSL") endif() - elseif(USE_WINHTTP) + elseif(WIN32) set(USE_HTTPS "WinHTTP") elseif(OPENSSL_FOUND) set(USE_HTTPS "OpenSSL") @@ -106,8 +106,27 @@ if(USE_HTTPS) # https://github.com/ARMmbed/mbedtls/issues/228 # For now, pass its link flags as our own list(APPEND LIBGIT2_PC_LIBS ${MBEDTLS_LIBRARIES}) + elseif(USE_HTTPS STREQUAL "Schannel") + set(GIT_SCHANNEL 1) + + list(APPEND LIBGIT2_SYSTEM_LIBS "rpcrt4" "crypt32" "ole32" "secur32") + list(APPEND LIBGIT2_PC_LIBS "-lrpcrt4" "-lcrypt32" "-lole32" "-lsecur32") elseif(USE_HTTPS STREQUAL "WinHTTP") - # WinHTTP setup was handled in the WinHTTP-specific block above + set(GIT_WINHTTP 1) + + # Since MinGW does not come with headers or an import library for winhttp, + # we have to include a private header and generate our own import library + if(MINGW) + add_subdirectory("${PROJECT_SOURCE_DIR}/deps/winhttp" "${PROJECT_BINARY_DIR}/deps/winhttp") + list(APPEND LIBGIT2_SYSTEM_LIBS winhttp) + list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${PROJECT_SOURCE_DIR}/deps/winhttp") + else() + list(APPEND LIBGIT2_SYSTEM_LIBS "winhttp") + list(APPEND LIBGIT2_PC_LIBS "-lwinhttp") + endif() + + list(APPEND LIBGIT2_SYSTEM_LIBS "rpcrt4" "crypt32" "ole32") + list(APPEND LIBGIT2_PC_LIBS "-lrpcrt4" "-lcrypt32" "-lole32") elseif(USE_HTTPS STREQUAL "OpenSSL-Dynamic") set(GIT_OPENSSL 1) set(GIT_OPENSSL_DYNAMIC 1) diff --git a/cmake/SelectHashes.cmake b/cmake/SelectHashes.cmake index faf9e2ea31d..5c007e58749 100644 --- a/cmake/SelectHashes.cmake +++ b/cmake/SelectHashes.cmake @@ -13,6 +13,8 @@ if(USE_SHA1 STREQUAL ON) elseif(USE_SHA1 STREQUAL "HTTPS") if(USE_HTTPS STREQUAL "SecureTransport") set(USE_SHA1 "CommonCrypto") + elseif(USE_HTTPS STREQUAL "Schannel") + set(USE_SHA1 "Win32") elseif(USE_HTTPS STREQUAL "WinHTTP") set(USE_SHA1 "Win32") elseif(USE_HTTPS) @@ -51,6 +53,8 @@ endif() if(USE_SHA256 STREQUAL "HTTPS") if(USE_HTTPS STREQUAL "SecureTransport") set(USE_SHA256 "CommonCrypto") + elseif(USE_HTTPS STREQUAL "Schannel") + set(USE_SHA256 "Win32") elseif(USE_HTTPS STREQUAL "WinHTTP") set(USE_SHA256 "Win32") elseif(USE_HTTPS) diff --git a/cmake/SelectWinHTTP.cmake b/cmake/SelectWinHTTP.cmake deleted file mode 100644 index 96e0bdbae54..00000000000 --- a/cmake/SelectWinHTTP.cmake +++ /dev/null @@ -1,17 +0,0 @@ -if(WIN32 AND USE_WINHTTP) - set(GIT_WINHTTP 1) - - # Since MinGW does not come with headers or an import library for winhttp, - # we have to include a private header and generate our own import library - if(MINGW) - add_subdirectory("${PROJECT_SOURCE_DIR}/deps/winhttp" "${PROJECT_BINARY_DIR}/deps/winhttp") - list(APPEND LIBGIT2_SYSTEM_LIBS winhttp) - list(APPEND LIBGIT2_DEPENDENCY_INCLUDES "${PROJECT_SOURCE_DIR}/deps/winhttp") - else() - list(APPEND LIBGIT2_SYSTEM_LIBS "winhttp") - list(APPEND LIBGIT2_PC_LIBS "-lwinhttp") - endif() - - list(APPEND LIBGIT2_SYSTEM_LIBS "rpcrt4" "crypt32" "ole32") - list(APPEND LIBGIT2_PC_LIBS "-lrpcrt4" "-lcrypt32" "-lole32") -endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e108b2e79ce..8fd22bc115c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,7 +42,6 @@ include(SelectHashes) include(SelectHTTPParser) include(SelectRegex) include(SelectSSH) -include(SelectWinHTTP) include(SelectZlib) # diff --git a/src/libgit2/libgit2.c b/src/libgit2/libgit2.c index f225122e548..5d796b1f79b 100644 --- a/src/libgit2/libgit2.c +++ b/src/libgit2/libgit2.c @@ -30,6 +30,7 @@ #include "streams/registry.h" #include "streams/mbedtls.h" #include "streams/openssl.h" +#include "streams/socket.h" #include "transports/smart.h" #include "transports/http.h" #include "transports/ssh.h" @@ -78,6 +79,7 @@ int git_libgit2_init(void) git_merge_driver_global_init, git_transport_ssh_global_init, git_stream_registry_global_init, + git_socket_stream_global_init, git_openssl_stream_global_init, git_mbedtls_stream_global_init, git_mwindow_global_init, diff --git a/src/libgit2/streams/schannel.c b/src/libgit2/streams/schannel.c new file mode 100644 index 00000000000..2c1066adb7d --- /dev/null +++ b/src/libgit2/streams/schannel.c @@ -0,0 +1,715 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "streams/schannel.h" + +#ifdef GIT_SCHANNEL + +#define SECURITY_WIN32 + +#include +#include +#include + +#include "stream.h" +#include "streams/socket.h" + +#ifndef SP_PROT_TLS1_2_CLIENT +# define SP_PROT_TLS1_2_CLIENT 2048 +#endif + +#ifndef SP_PROT_TLS1_3_CLIENT +# define SP_PROT_TLS1_3_CLIENT 8192 +#endif + +#ifndef SECBUFFER_ALERT +# define SECBUFFER_ALERT 17 +#endif + +#define READ_BLOCKSIZE (16 * 1024) + +typedef enum { + STATE_NONE = 0, + STATE_CRED = 1, + STATE_CONTEXT = 2, + STATE_CERTIFICATE = 3 +} schannel_state; + +typedef struct { + git_stream parent; + git_stream *io; + int owned; + bool connected; + wchar_t *host_w; + + schannel_state state; + + CredHandle cred; + CtxtHandle context; + SecPkgContext_StreamSizes stream_sizes; + + CERT_CONTEXT *certificate; + const CERT_CHAIN_CONTEXT *cert_chain; + git_cert_x509 x509; + + git_str plaintext_in; + git_str ciphertext_in; +} schannel_stream; + +static int connect_context(schannel_stream *st) +{ + SCHANNEL_CRED cred = { 0 }; + SECURITY_STATUS status = SEC_E_INTERNAL_ERROR; + DWORD context_flags; + static size_t MAX_RETRIES = 1024; + size_t retries; + ssize_t read_len; + int error = 0; + + if (st->owned && (error = git_stream_connect(st->io)) < 0) + return error; + + cred.dwVersion = SCHANNEL_CRED_VERSION; + cred.dwFlags = SCH_CRED_IGNORE_NO_REVOCATION_CHECK | + SCH_CRED_IGNORE_REVOCATION_OFFLINE | + SCH_CRED_MANUAL_CRED_VALIDATION | + SCH_CRED_NO_DEFAULT_CREDS | + SCH_CRED_NO_SERVERNAME_CHECK; + cred.grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT | + SP_PROT_TLS1_3_CLIENT; + + if (AcquireCredentialsHandleW(NULL, SCHANNEL_NAME_W, + SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, + NULL, &st->cred, NULL) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not acquire credentials handle"); + return -1; + } + + st->state = STATE_CRED; + + context_flags = ISC_REQ_ALLOCATE_MEMORY | + ISC_REQ_CONFIDENTIALITY | + ISC_REQ_REPLAY_DETECT | + ISC_REQ_SEQUENCE_DETECT | + ISC_REQ_STREAM; + + for (retries = 0; retries < MAX_RETRIES; retries++) { + SecBuffer input_buf[] = { + { (unsigned long)st->ciphertext_in.size, + SECBUFFER_TOKEN, + st->ciphertext_in.size ? st->ciphertext_in.ptr : NULL }, + { 0, SECBUFFER_EMPTY, NULL } + }; + SecBuffer output_buf[] = { { 0, SECBUFFER_TOKEN, NULL }, + { 0, SECBUFFER_ALERT, NULL } }; + + SecBufferDesc input_buf_desc = { SECBUFFER_VERSION, 2, input_buf }; + SecBufferDesc output_buf_desc = { SECBUFFER_VERSION, 2, output_buf }; + + status = InitializeSecurityContextW(&st->cred, + retries ? &st->context : NULL, st->host_w, + context_flags, 0, 0, retries ? &input_buf_desc : NULL, 0, + retries ? NULL : &st->context, &output_buf_desc, + &context_flags, NULL); + + if (status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED) { + st->state = STATE_CONTEXT; + + if (output_buf[0].cbBuffer > 0) { + error = git_stream__write_full(st->io, + output_buf[0].pvBuffer, + output_buf[0].cbBuffer, 0); + + FreeContextBuffer(output_buf[0].pvBuffer); + } + + /* handle any leftover, unprocessed data */ + if (input_buf[1].BufferType == SECBUFFER_EXTRA) { + GIT_ASSERT(st->ciphertext_in.size > input_buf[1].cbBuffer); + + git_str_consume_bytes(&st->ciphertext_in, + st->ciphertext_in.size - input_buf[1].cbBuffer); + } else { + git_str_clear(&st->ciphertext_in); + } + + if (error < 0 || status == SEC_E_OK) + break; + } else if (status == SEC_E_INCOMPLETE_MESSAGE) { + /* we need additional data from the client; */ + if (git_str_grow_by(&st->ciphertext_in, READ_BLOCKSIZE) < 0) { + error = -1; + break; + } + + if ((read_len = git_stream_read(st->io, + st->ciphertext_in.ptr + st->ciphertext_in.size, + (st->ciphertext_in.asize - st->ciphertext_in.size))) < 0) { + error = -1; + break; + } + + GIT_ASSERT((size_t)read_len <= + st->ciphertext_in.asize - st->ciphertext_in.size); + st->ciphertext_in.size += read_len; + } else { + git_error_set(GIT_ERROR_OS, + "could not initialize security context"); + error = -1; + break; + } + + GIT_ASSERT(st->ciphertext_in.size < ULONG_MAX); + } + + if (retries == MAX_RETRIES) { + git_error_set(GIT_ERROR_SSL, + "could not initialize security context: too many retries"); + error = -1; + } + + if (!error) { + if (QueryContextAttributesW(&st->context, + SECPKG_ATTR_STREAM_SIZES, + &st->stream_sizes) != SEC_E_OK) { + git_error_set(GIT_ERROR_SSL, + "could not query stream sizes"); + error = -1; + } + } + + return error; +} + +static int set_certificate_lookup_error(DWORD status) +{ + switch (status) { + case CERT_TRUST_IS_NOT_TIME_VALID: + git_error_set(GIT_ERROR_SSL, + "certificate is expired or not yet valid"); + break; + case CERT_TRUST_IS_REVOKED: + git_error_set(GIT_ERROR_SSL, "certificate is revoked"); + break; + case CERT_TRUST_IS_NOT_SIGNATURE_VALID: + case CERT_TRUST_IS_NOT_VALID_FOR_USAGE: + case CERT_TRUST_INVALID_EXTENSION: + case CERT_TRUST_INVALID_POLICY_CONSTRAINTS: + case CERT_TRUST_INVALID_BASIC_CONSTRAINTS: + case CERT_TRUST_INVALID_NAME_CONSTRAINTS: + case CERT_TRUST_HAS_NOT_SUPPORTED_NAME_CONSTRAINT: + case CERT_TRUST_HAS_NOT_DEFINED_NAME_CONSTRAINT: + case CERT_TRUST_HAS_NOT_PERMITTED_NAME_CONSTRAINT: + case CERT_TRUST_HAS_EXCLUDED_NAME_CONSTRAINT: + case CERT_TRUST_NO_ISSUANCE_CHAIN_POLICY: + case CERT_TRUST_HAS_NOT_SUPPORTED_CRITICAL_EXT: + git_error_set(GIT_ERROR_SSL, "certificate is not valid"); + break; + case CERT_TRUST_IS_UNTRUSTED_ROOT: + case CERT_TRUST_IS_CYCLIC: + case CERT_TRUST_IS_EXPLICIT_DISTRUST: + git_error_set(GIT_ERROR_SSL, "certificate is not trusted"); + break; + case CERT_TRUST_REVOCATION_STATUS_UNKNOWN: + git_error_set(GIT_ERROR_SSL, + "certificate revocation status could not be verified"); + break; + case CERT_TRUST_IS_OFFLINE_REVOCATION: + git_error_set(GIT_ERROR_SSL, + "certificate revocation is offline or stale"); + break; + case CERT_TRUST_HAS_WEAK_SIGNATURE: + git_error_set(GIT_ERROR_SSL, "certificate has a weak signature"); + break; + default: + git_error_set(GIT_ERROR_SSL, + "unknown certificate lookup failure: %d", status); + return -1; + } + + return GIT_ECERTIFICATE; +} + +static int set_certificate_validation_error(DWORD status) +{ + switch (status) { + case TRUST_E_CERT_SIGNATURE: + git_error_set(GIT_ERROR_SSL, + "the certificate cannot be verified"); + break; + case CRYPT_E_REVOKED: + git_error_set(GIT_ERROR_SSL, + "the certificate or signature has been revoked"); + break; + case CERT_E_UNTRUSTEDROOT: + git_error_set(GIT_ERROR_SSL, + "the certificate root is not trusted"); + break; + case CERT_E_UNTRUSTEDTESTROOT: + git_error_set(GIT_ERROR_SSL, + "the certificate root is a test certificate"); + break; + case CERT_E_CHAINING: + git_error_set(GIT_ERROR_SSL, + "the certificate chain is invalid"); + break; + case CERT_E_WRONG_USAGE: + case CERT_E_PURPOSE: + git_error_set(GIT_ERROR_SSL, + "the certificate is not valid for this usage"); + break; + case CERT_E_EXPIRED: + git_error_set(GIT_ERROR_SSL, + "certificate is expired or not yet valid"); + break; + case CERT_E_INVALID_NAME: + case CERT_E_CN_NO_MATCH: + git_error_set(GIT_ERROR_SSL, + "certificate is not valid for this hostname"); + break; + case CERT_E_INVALID_POLICY: + case TRUST_E_BASIC_CONSTRAINTS: + case CERT_E_CRITICAL: + case CERT_E_VALIDITYPERIODNESTING: + git_error_set(GIT_ERROR_SSL, "certificate is not valid"); + break; + case CRYPT_E_NO_REVOCATION_CHECK: + git_error_set(GIT_ERROR_SSL, + "certificate revocation status could not be verified"); + break; + case CRYPT_E_REVOCATION_OFFLINE: + git_error_set(GIT_ERROR_SSL, + "certificate revocation is offline or stale"); + break; + case CERT_E_ROLE: + git_error_set(GIT_ERROR_SSL, "certificate authority is not valid"); + break; + default: + git_error_set(GIT_ERROR_SSL, + "unknown certificate policy checking failure: %d", + status); + return -1; + } + + return GIT_ECERTIFICATE; +} + +static int check_certificate(schannel_stream* st) +{ + CERT_CHAIN_PARA cert_chain_parameters; + SSL_EXTRA_CERT_CHAIN_POLICY_PARA ssl_policy_parameters; + CERT_CHAIN_POLICY_PARA cert_policy_parameters = + { sizeof(CERT_CHAIN_POLICY_PARA), 0, &ssl_policy_parameters }; + CERT_CHAIN_POLICY_STATUS cert_policy_status; + + memset(&cert_chain_parameters, 0, sizeof(CERT_CHAIN_PARA)); + cert_chain_parameters.cbSize = sizeof(CERT_CHAIN_PARA); + + if (QueryContextAttributesW(&st->context, + SECPKG_ATTR_REMOTE_CERT_CONTEXT, + &st->certificate) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, + "could not query remote certificate context"); + return -1; + } + + /* TODO: do we really want to do revokcation checking ? */ + if (!CertGetCertificateChain(NULL, st->certificate, NULL, + st->certificate->hCertStore, &cert_chain_parameters, + CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT, + NULL, &st->cert_chain)) { + git_error_set(GIT_ERROR_OS, "could not query remote certificate chain"); + CertFreeCertificateContext(st->certificate); + return -1; + } + + st->state = STATE_CERTIFICATE; + + /* Set up the x509 certificate data for future callbacks */ + + st->x509.parent.cert_type = GIT_CERT_X509; + st->x509.data = st->certificate->pbCertEncoded; + st->x509.len = st->certificate->cbCertEncoded; + + /* Handle initial certificate validation */ + + if (st->cert_chain->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) + return set_certificate_lookup_error(st->cert_chain->TrustStatus.dwErrorStatus); + + ssl_policy_parameters.cbSize = sizeof(SSL_EXTRA_CERT_CHAIN_POLICY_PARA); + ssl_policy_parameters.dwAuthType = AUTHTYPE_SERVER; + ssl_policy_parameters.pwszServerName = st->host_w; + + if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, + st->cert_chain, &cert_policy_parameters, + &cert_policy_status)) { + git_error_set(GIT_ERROR_OS, "could not verify certificate chain policy"); + return -1; + } + + if (cert_policy_status.dwError != SEC_E_OK) + return set_certificate_validation_error(cert_policy_status.dwError); + + return 0; +} + +static int schannel_connect(git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + int error; + + GIT_ASSERT(st->state == STATE_NONE); + + if ((error = connect_context(st)) < 0 || + (error = check_certificate(st)) < 0) + return error; + + st->connected = 1; + return 0; +} + +static int schannel_certificate(git_cert **out, git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + + *out = &st->x509.parent; + return 0; +} + +static int schannel_set_proxy( + git_stream *stream, + const git_proxy_options *proxy_options) +{ + schannel_stream *st = (schannel_stream *)stream; + return git_stream_set_proxy(st->io, proxy_options); +} + +static ssize_t schannel_write( + git_stream *stream, + const char *data, + size_t data_len, + int flags) +{ + schannel_stream *st = (schannel_stream *)stream; + SecBuffer encrypt_buf[3]; + SecBufferDesc encrypt_buf_desc = { SECBUFFER_VERSION, 3, encrypt_buf }; + git_str ciphertext_out = GIT_STR_INIT; + ssize_t total_len = 0; + + GIT_UNUSED(flags); + + if (data_len > SSIZE_MAX) + data_len = SSIZE_MAX; + + git_str_init(&ciphertext_out, + st->stream_sizes.cbHeader + + st->stream_sizes.cbMaximumMessage + + st->stream_sizes.cbTrailer); + + while (data_len > 0) { + size_t message_len = min(data_len, st->stream_sizes.cbMaximumMessage); + size_t ciphertext_len, ciphertext_written = 0; + + encrypt_buf[0].BufferType = SECBUFFER_STREAM_HEADER; + encrypt_buf[0].cbBuffer = st->stream_sizes.cbHeader; + encrypt_buf[0].pvBuffer = ciphertext_out.ptr; + + encrypt_buf[1].BufferType = SECBUFFER_DATA; + encrypt_buf[1].cbBuffer = (unsigned long)message_len; + encrypt_buf[1].pvBuffer = + ciphertext_out.ptr + st->stream_sizes.cbHeader; + + encrypt_buf[2].BufferType = SECBUFFER_STREAM_TRAILER; + encrypt_buf[2].cbBuffer = st->stream_sizes.cbTrailer; + encrypt_buf[2].pvBuffer = + ciphertext_out.ptr + st->stream_sizes.cbHeader + + message_len; + + memcpy(ciphertext_out.ptr + st->stream_sizes.cbHeader, data, message_len); + + if (EncryptMessage(&st->context, 0, &encrypt_buf_desc, 0) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not encrypt tls message"); + total_len = -1; + goto done; + } + + ciphertext_len = encrypt_buf[0].cbBuffer + + encrypt_buf[1].cbBuffer + + encrypt_buf[2].cbBuffer; + + while (ciphertext_written < ciphertext_len) { + ssize_t chunk_len = git_stream_write(st->io, + ciphertext_out.ptr + ciphertext_written, + ciphertext_len - ciphertext_written, 0); + + if (chunk_len < 0) { + total_len = -1; + goto done; + } + + ciphertext_len -= chunk_len; + ciphertext_written += chunk_len; + } + + total_len += message_len; + + data += message_len; + data_len -= message_len; + } + +done: + git_str_dispose(&ciphertext_out); + return total_len; +} + +static ssize_t schannel_read(git_stream *stream, void *_data, size_t data_len) +{ + schannel_stream *st = (schannel_stream *)stream; + char *data = (char *)_data; + SecBuffer decrypt_buf[4]; + SecBufferDesc decrypt_buf_desc = { SECBUFFER_VERSION, 4, decrypt_buf }; + SECURITY_STATUS status; + ssize_t chunk_len, total_len = 0; + + if (data_len > SSIZE_MAX) + data_len = SSIZE_MAX; + + /* + * Loop until we have some bytes to return - we may have decrypted + * bytes queued or ciphertext from the wire that we can decrypt and + * return. Return any queued bytes if they're available to avoid a + * network read, which may block. We may return less than the + * caller requested, and they can retry for an actual network + */ + while ((size_t)total_len < data_len) { + if (st->plaintext_in.size > 0) { + size_t copy_len = min(st->plaintext_in.size, data_len); + + memcpy(data, st->plaintext_in.ptr, copy_len); + git_str_consume_bytes(&st->plaintext_in, copy_len); + + data += copy_len; + data_len -= copy_len; + + total_len += copy_len; + + continue; + } + + if (st->ciphertext_in.size > 0) { + decrypt_buf[0].BufferType = SECBUFFER_DATA; + decrypt_buf[0].cbBuffer = (unsigned long)min(st->ciphertext_in.size, ULONG_MAX); + decrypt_buf[0].pvBuffer = st->ciphertext_in.ptr; + + decrypt_buf[1].BufferType = SECBUFFER_EMPTY; + decrypt_buf[1].cbBuffer = 0; + decrypt_buf[1].pvBuffer = NULL; + + decrypt_buf[2].BufferType = SECBUFFER_EMPTY; + decrypt_buf[2].cbBuffer = 0; + decrypt_buf[2].pvBuffer = NULL; + + decrypt_buf[3].BufferType = SECBUFFER_EMPTY; + decrypt_buf[3].cbBuffer = 0; + decrypt_buf[3].pvBuffer = NULL; + + status = DecryptMessage(&st->context, &decrypt_buf_desc, 0, NULL); + + if (status == SEC_E_OK) { + GIT_ASSERT(decrypt_buf[0].BufferType == SECBUFFER_STREAM_HEADER); + GIT_ASSERT(decrypt_buf[1].BufferType == SECBUFFER_DATA); + GIT_ASSERT(decrypt_buf[2].BufferType == SECBUFFER_STREAM_TRAILER); + + if (git_str_put(&st->plaintext_in, decrypt_buf[1].pvBuffer, decrypt_buf[1].cbBuffer) < 0) { + total_len = -1; + goto done; + } + + if (decrypt_buf[3].BufferType == SECBUFFER_EXTRA) { + git_str_consume_bytes(&st->ciphertext_in, (st->ciphertext_in.size - decrypt_buf[3].cbBuffer)); + } else { + git_str_clear(&st->ciphertext_in); + } + + continue; + } else if (status == SEC_E_CONTEXT_EXPIRED) { + break; + } else if (status != SEC_E_INCOMPLETE_MESSAGE) { + git_error_set(GIT_ERROR_SSL, "could not decrypt tls message"); + total_len = -1; + goto done; + } + } + + if (total_len != 0) + break; + + if (git_str_grow_by(&st->ciphertext_in, READ_BLOCKSIZE) < 0) { + total_len = -1; + goto done; + } + + if ((chunk_len = git_stream_read(st->io, st->ciphertext_in.ptr + st->ciphertext_in.size, st->ciphertext_in.asize - st->ciphertext_in.size)) < 0) { + total_len = -1; + goto done; + } + + st->ciphertext_in.size += chunk_len; + } + +done: + return total_len; +} + +static int schannel_close(git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + int error = 0; + + if (st->connected) { + SecBuffer shutdown_buf; + SecBufferDesc shutdown_buf_desc = + { SECBUFFER_VERSION, 1, &shutdown_buf }; + DWORD shutdown_message = SCHANNEL_SHUTDOWN, shutdown_flags; + + shutdown_buf.BufferType = SECBUFFER_TOKEN; + shutdown_buf.cbBuffer = sizeof(DWORD); + shutdown_buf.pvBuffer = &shutdown_message; + + if (ApplyControlToken(&st->context, &shutdown_buf_desc) != SEC_E_OK) { + git_error_set(GIT_ERROR_SSL, "could not shutdown stream"); + error = -1; + } + + shutdown_buf.BufferType = SECBUFFER_TOKEN; + shutdown_buf.cbBuffer = 0; + shutdown_buf.pvBuffer = NULL; + + shutdown_flags = ISC_REQ_ALLOCATE_MEMORY | + ISC_REQ_CONFIDENTIALITY | + ISC_REQ_REPLAY_DETECT | + ISC_REQ_SEQUENCE_DETECT | + ISC_REQ_STREAM; + + if (InitializeSecurityContext(&st->cred, &st->context, + NULL, shutdown_flags, 0, 0, + &shutdown_buf_desc, 0, NULL, + &shutdown_buf_desc, &shutdown_flags, + NULL) == SEC_E_OK) { + if (shutdown_buf.cbBuffer > 0) { + if (git_stream__write_full(st->io, + shutdown_buf.pvBuffer, + shutdown_buf.cbBuffer, 0) < 0) + error = -1; + + FreeContextBuffer(shutdown_buf.pvBuffer); + } + } + } + + st->connected = false; + + if (st->owned && git_stream_close(st->io) < 0) + error = -1; + + return error; +} + +static void schannel_free(git_stream *stream) +{ + schannel_stream *st = (schannel_stream *)stream; + + if (st->state >= STATE_CERTIFICATE) { + CertFreeCertificateContext(st->certificate); + CertFreeCertificateChain(st->cert_chain); + } + + if (st->state >= STATE_CONTEXT) + DeleteSecurityContext(&st->context); + + if (st->state >= STATE_CRED) + FreeCredentialsHandle(&st->cred); + + st->state = STATE_NONE; + + git_str_dispose(&st->ciphertext_in); + git_str_dispose(&st->plaintext_in); + + git__free(st->host_w); + + if (st->owned) + git_stream_free(st->io); + + git__free(st); +} + +static int schannel_stream_wrap( + git_stream **out, + git_stream *in, + const char *host, + int owned) +{ + schannel_stream *st; + + st = git__calloc(1, sizeof(schannel_stream)); + GIT_ERROR_CHECK_ALLOC(st); + + st->io = in; + st->owned = owned; + + if (git__utf8_to_16_alloc(&st->host_w, host) < 0) { + git__free(st); + return -1; + } + + st->parent.version = GIT_STREAM_VERSION; + st->parent.encrypted = 1; + st->parent.proxy_support = git_stream_supports_proxy(st->io); + st->parent.connect = schannel_connect; + st->parent.certificate = schannel_certificate; + st->parent.set_proxy = schannel_set_proxy; + st->parent.read = schannel_read; + st->parent.write = schannel_write; + st->parent.close = schannel_close; + st->parent.free = schannel_free; + + *out = (git_stream *)st; + return 0; +} + +extern int git_schannel_stream_new( + git_stream **out, + const char *host, + const char *port) +{ + git_stream *stream; + int error; + + GIT_ASSERT_ARG(out); + GIT_ASSERT_ARG(host); + GIT_ASSERT_ARG(port); + + if ((error = git_socket_stream_new(&stream, host, port)) < 0) + return error; + + if ((error = schannel_stream_wrap(out, stream, host, 1)) < 0) { + git_stream_close(stream); + git_stream_free(stream); + } + + return error; +} + +extern int git_schannel_stream_wrap( + git_stream **out, + git_stream *in, + const char *host) +{ + return schannel_stream_wrap(out, in, host, 0); +} + +#endif diff --git a/src/libgit2/streams/schannel.h b/src/libgit2/streams/schannel.h new file mode 100644 index 00000000000..3584970d1f8 --- /dev/null +++ b/src/libgit2/streams/schannel.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_steams_schannel_h__ +#define INCLUDE_steams_schannel_h__ + +#include "common.h" + +#include "git2/sys/stream.h" + +#ifdef GIT_SCHANNEL + +extern int git_schannel_stream_new( + git_stream **out, + const char *host, + const char *port); + +extern int git_schannel_stream_wrap( + git_stream **out, + git_stream *in, + const char *host); + +#endif + +#endif diff --git a/src/libgit2/streams/socket.c b/src/libgit2/streams/socket.c index 908e8c02f9b..8f23e746e51 100644 --- a/src/libgit2/streams/socket.c +++ b/src/libgit2/streams/socket.c @@ -10,22 +10,23 @@ #include "posix.h" #include "netops.h" #include "registry.h" +#include "runtime.h" #include "stream.h" #ifndef _WIN32 -# include -# include -# include -# include -# include -# include -# include +# include +# include +# include +# include +# include +# include +# include #else -# include -# include -# ifdef _MSC_VER -# pragma comment(lib, "ws2_32") -# endif +# include +# include +# ifdef _MSC_VER +# pragma comment(lib, "ws2_32") +# endif #endif #ifdef GIT_WIN32 @@ -54,11 +55,8 @@ static int close_socket(GIT_SOCKET s) return 0; #ifdef GIT_WIN32 - if (SOCKET_ERROR == closesocket(s)) - return -1; - - if (0 != WSACleanup()) { - git_error_set(GIT_ERROR_OS, "winsock cleanup failed"); + if (closesocket(s) != 0) { + net_set_error("could not close socket"); return -1; } @@ -77,23 +75,6 @@ static int socket_connect(git_stream *stream) GIT_SOCKET s = INVALID_SOCKET; int ret; -#ifdef GIT_WIN32 - /* on win32, the WSA context needs to be initialized - * before any socket calls can be performed */ - WSADATA wsd; - - if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) { - git_error_set(GIT_ERROR_OS, "winsock init failed"); - return -1; - } - - if (LOBYTE(wsd.wVersion) != 2 || HIBYTE(wsd.wVersion) != 2) { - WSACleanup(); - git_error_set(GIT_ERROR_OS, "winsock init failed"); - return -1; - } -#endif - memset(&hints, 0x0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; hints.ai_family = AF_UNSPEC; @@ -240,3 +221,42 @@ int git_socket_stream_new( return init(out, host, port); } + +#ifdef GIT_WIN32 + +static void socket_stream_global_shutdown(void) +{ + WSACleanup(); +} + +int git_socket_stream_global_init(void) +{ + WORD winsock_version; + WSADATA wsa_data; + + winsock_version = MAKEWORD(2, 2); + + if (WSAStartup(winsock_version, &wsa_data) != 0) { + git_error_set(GIT_ERROR_OS, "could not initialize Windows Socket Library"); + return -1; + } + + if (LOBYTE(wsa_data.wVersion) != 2 || + HIBYTE(wsa_data.wVersion) != 2) { + git_error_set(GIT_ERROR_SSL, "Windows Socket Library does not support Winsock 2.2"); + return -1; + } + + return git_runtime_shutdown_register(socket_stream_global_shutdown); +} + +#else + +#include "stream.h" + +int git_socket_stream_global_init(void) +{ + return 0; +} + + #endif diff --git a/src/libgit2/streams/socket.h b/src/libgit2/streams/socket.h index 3235f31679c..300e708937f 100644 --- a/src/libgit2/streams/socket.h +++ b/src/libgit2/streams/socket.h @@ -20,4 +20,6 @@ typedef struct { extern int git_socket_stream_new(git_stream **out, const char *host, const char *port); +extern int git_socket_stream_global_init(void); + #endif diff --git a/src/libgit2/streams/tls.c b/src/libgit2/streams/tls.c index e063a33f99a..246ac9ca793 100644 --- a/src/libgit2/streams/tls.c +++ b/src/libgit2/streams/tls.c @@ -13,6 +13,7 @@ #include "streams/mbedtls.h" #include "streams/openssl.h" #include "streams/stransport.h" +#include "streams/schannel.h" int git_tls_stream_new(git_stream **out, const char *host, const char *port) { @@ -33,6 +34,8 @@ int git_tls_stream_new(git_stream **out, const char *host, const char *port) init = git_openssl_stream_new; #elif defined(GIT_MBEDTLS) init = git_mbedtls_stream_new; +#elif defined(GIT_SCHANNEL) + init = git_schannel_stream_new; #endif } else { return error; @@ -63,6 +66,8 @@ int git_tls_stream_wrap(git_stream **out, git_stream *in, const char *host) wrap = git_openssl_stream_wrap; #elif defined(GIT_MBEDTLS) wrap = git_mbedtls_stream_wrap; +#elif defined(GIT_SCHANNEL) + wrap = git_schannel_stream_wrap; #endif } diff --git a/src/util/git2_features.h.in b/src/util/git2_features.h.in index fbf0cab60ea..1575be641e1 100644 --- a/src/util/git2_features.h.in +++ b/src/util/git2_features.h.in @@ -41,6 +41,7 @@ #cmakedefine GIT_OPENSSL_DYNAMIC 1 #cmakedefine GIT_SECURE_TRANSPORT 1 #cmakedefine GIT_MBEDTLS 1 +#cmakedefine GIT_SCHANNEL 1 #cmakedefine GIT_SHA1_COLLISIONDETECT 1 #cmakedefine GIT_SHA1_WIN32 1 diff --git a/tests/libgit2/online/clone.c b/tests/libgit2/online/clone.c index 1a4cdb520ce..b635739b676 100644 --- a/tests/libgit2/online/clone.c +++ b/tests/libgit2/online/clone.c @@ -580,6 +580,17 @@ static int succeed_certificate_check(git_cert *cert, int valid, const char *host return 0; } +static int x509_succeed_certificate_check(git_cert *cert, int valid, const char *host, void *payload) +{ + GIT_UNUSED(valid); + GIT_UNUSED(payload); + + cl_assert_equal_s("github.com", host); + cl_assert_equal_i(GIT_CERT_X509, cert->cert_type); + + return 0; +} + static int fail_certificate_check(git_cert *cert, int valid, const char *host, void *payload) { GIT_UNUSED(cert); @@ -901,7 +912,7 @@ void test_online_clone__certificate_invalid(void) void test_online_clone__certificate_valid(void) { - g_options.fetch_opts.callbacks.certificate_check = succeed_certificate_check; + g_options.fetch_opts.callbacks.certificate_check = x509_succeed_certificate_check; cl_git_pass(git_clone(&g_repo, "https://github.com/libgit2/TestGitRepository", "./foo", &g_options)); } diff --git a/tests/libgit2/stream/registration.c b/tests/libgit2/stream/registration.c index bf3c20502a0..ccaecee8c1e 100644 --- a/tests/libgit2/stream/registration.c +++ b/tests/libgit2/stream/registration.c @@ -81,10 +81,10 @@ void test_stream_registration__tls(void) cl_git_pass(git_stream_register(GIT_STREAM_TLS, NULL)); error = git_tls_stream_new(&stream, "localhost", "443"); - /* We don't have TLS support enabled, or we're on Windows, - * which has no arbitrary TLS stream support. + /* We don't have TLS support enabled, or we're on Windows + * with WinHTTP, which is not actually TLS stream support. */ -#if defined(GIT_WIN32) || !defined(GIT_HTTPS) +#if defined(GIT_WINHTTP) || !defined(GIT_HTTPS) cl_git_fail_with(-1, error); #else cl_git_pass(error); From cff0d9b1cc2432ab6a6eab777cdfe230ee07fca1 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 11 Mar 2023 00:03:34 -0800 Subject: [PATCH 2/4] http: refactor GSSAPI / negotiate / NTLM auth Name the GSSAPI and ntlmclient authentication providers as such. Today they're named after the authentication mechanism ("Negotiate", "NTLM") instead of their implementation. If we have competing implementations for the same mechanism (eg, a future Windows SSPI-based provider for Negotiate and NTLM) then this will get confusing. --- .../{auth_negotiate.c => auth_gssapi.c} | 64 +++++++++---------- src/libgit2/transports/auth_ntlm.h | 4 +- .../{auth_ntlm.c => auth_ntlmclient.c} | 24 +++---- 3 files changed, 46 insertions(+), 46 deletions(-) rename src/libgit2/transports/{auth_negotiate.c => auth_gssapi.c} (79%) rename src/libgit2/transports/{auth_ntlm.c => auth_ntlmclient.c} (88%) diff --git a/src/libgit2/transports/auth_negotiate.c b/src/libgit2/transports/auth_gssapi.c similarity index 79% rename from src/libgit2/transports/auth_negotiate.c rename to src/libgit2/transports/auth_gssapi.c index 6380504be7e..5005538411b 100644 --- a/src/libgit2/transports/auth_negotiate.c +++ b/src/libgit2/transports/auth_gssapi.c @@ -20,13 +20,13 @@ #include #endif -static gss_OID_desc negotiate_oid_spnego = +static gss_OID_desc gssapi_oid_spnego = { 6, (void *) "\x2b\x06\x01\x05\x05\x02" }; -static gss_OID_desc negotiate_oid_krb5 = +static gss_OID_desc gssapi_oid_krb5 = { 9, (void *) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" }; -static gss_OID negotiate_oids[] = - { &negotiate_oid_spnego, &negotiate_oid_krb5, NULL }; +static gss_OID gssapi_oids[] = + { &gssapi_oid_spnego, &gssapi_oid_krb5, NULL }; typedef struct { git_http_auth_context parent; @@ -36,9 +36,9 @@ typedef struct { char *challenge; gss_ctx_id_t gss_context; gss_OID oid; -} http_auth_negotiate_context; +} http_auth_gssapi_context; -static void negotiate_err_set( +static void gssapi_err_set( OM_uint32 status_major, OM_uint32 status_minor, const char *message) @@ -58,11 +58,11 @@ static void negotiate_err_set( } } -static int negotiate_set_challenge( +static int gssapi_set_challenge( git_http_auth_context *c, const char *challenge) { - http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; GIT_ASSERT_ARG(ctx); GIT_ASSERT_ARG(challenge); @@ -76,7 +76,7 @@ static int negotiate_set_challenge( return 0; } -static void negotiate_context_dispose(http_auth_negotiate_context *ctx) +static void gssapi_context_dispose(http_auth_gssapi_context *ctx) { OM_uint32 status_minor; @@ -92,12 +92,12 @@ static void negotiate_context_dispose(http_auth_negotiate_context *ctx) ctx->challenge = NULL; } -static int negotiate_next_token( +static int gssapi_next_token( git_str *buf, git_http_auth_context *c, git_credential *cred) { - http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; OM_uint32 status_major, status_minor; gss_buffer_desc target_buffer = GSS_C_EMPTY_BUFFER, input_token = GSS_C_EMPTY_BUFFER, @@ -126,7 +126,7 @@ static int negotiate_next_token( GSS_C_NT_HOSTBASED_SERVICE, &server); if (GSS_ERROR(status_major)) { - negotiate_err_set(status_major, status_minor, + gssapi_err_set(status_major, status_minor, "could not parse principal"); error = -1; goto done; @@ -152,10 +152,10 @@ static int negotiate_next_token( input_token.length = input_buf.size; input_token_ptr = &input_token; } else if (ctx->gss_context != GSS_C_NO_CONTEXT) { - negotiate_context_dispose(ctx); + gssapi_context_dispose(ctx); } - mech = &negotiate_oid_spnego; + mech = &gssapi_oid_spnego; status_major = gss_init_sec_context( &status_minor, @@ -173,14 +173,14 @@ static int negotiate_next_token( NULL); if (GSS_ERROR(status_major)) { - negotiate_err_set(status_major, status_minor, "negotiate failure"); + gssapi_err_set(status_major, status_minor, "negotiate failure"); error = -1; goto done; } /* This message merely told us auth was complete; we do not respond. */ if (status_major == GSS_S_COMPLETE) { - negotiate_context_dispose(ctx); + gssapi_context_dispose(ctx); ctx->complete = 1; goto done; } @@ -204,20 +204,20 @@ static int negotiate_next_token( return error; } -static int negotiate_is_complete(git_http_auth_context *c) +static int gssapi_is_complete(git_http_auth_context *c) { - http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; GIT_ASSERT_ARG(ctx); return (ctx->complete == 1); } -static void negotiate_context_free(git_http_auth_context *c) +static void gssapi_context_free(git_http_auth_context *c) { - http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + http_auth_gssapi_context *ctx = (http_auth_gssapi_context *)c; - negotiate_context_dispose(ctx); + gssapi_context_dispose(ctx); ctx->configured = 0; ctx->complete = 0; @@ -226,8 +226,8 @@ static void negotiate_context_free(git_http_auth_context *c) git__free(ctx); } -static int negotiate_init_context( - http_auth_negotiate_context *ctx, +static int gssapi_init_context( + http_auth_gssapi_context *ctx, const git_net_url *url) { OM_uint32 status_major, status_minor; @@ -239,13 +239,13 @@ static int negotiate_init_context( status_major = gss_indicate_mechs(&status_minor, &mechanism_list); if (GSS_ERROR(status_major)) { - negotiate_err_set(status_major, status_minor, + gssapi_err_set(status_major, status_minor, "could not query mechanisms"); return -1; } if (mechanism_list) { - for (oid = negotiate_oids; *oid; oid++) { + for (oid = gssapi_oids; *oid; oid++) { for (i = 0; i < mechanism_list->count; i++) { item = &mechanism_list->elements[i]; @@ -285,14 +285,14 @@ int git_http_auth_negotiate( git_http_auth_context **out, const git_net_url *url) { - http_auth_negotiate_context *ctx; + http_auth_gssapi_context *ctx; *out = NULL; - ctx = git__calloc(1, sizeof(http_auth_negotiate_context)); + ctx = git__calloc(1, sizeof(http_auth_gssapi_context)); GIT_ERROR_CHECK_ALLOC(ctx); - if (negotiate_init_context(ctx, url) < 0) { + if (gssapi_init_context(ctx, url) < 0) { git__free(ctx); return -1; } @@ -300,10 +300,10 @@ int git_http_auth_negotiate( ctx->parent.type = GIT_HTTP_AUTH_NEGOTIATE; ctx->parent.credtypes = GIT_CREDENTIAL_DEFAULT; ctx->parent.connection_affinity = 1; - ctx->parent.set_challenge = negotiate_set_challenge; - ctx->parent.next_token = negotiate_next_token; - ctx->parent.is_complete = negotiate_is_complete; - ctx->parent.free = negotiate_context_free; + ctx->parent.set_challenge = gssapi_set_challenge; + ctx->parent.next_token = gssapi_next_token; + ctx->parent.is_complete = gssapi_is_complete; + ctx->parent.free = gssapi_context_free; *out = (git_http_auth_context *)ctx; diff --git a/src/libgit2/transports/auth_ntlm.h b/src/libgit2/transports/auth_ntlm.h index 40689498cae..2bf8f41f619 100644 --- a/src/libgit2/transports/auth_ntlm.h +++ b/src/libgit2/transports/auth_ntlm.h @@ -5,8 +5,8 @@ * a Linking Exception. For full terms see the included COPYING file. */ -#ifndef INCLUDE_transports_auth_ntlm_h__ -#define INCLUDE_transports_auth_ntlm_h__ +#ifndef INCLUDE_transports_auth_ntlmclient_h__ +#define INCLUDE_transports_auth_ntlmclient_h__ #include "auth.h" diff --git a/src/libgit2/transports/auth_ntlm.c b/src/libgit2/transports/auth_ntlmclient.c similarity index 88% rename from src/libgit2/transports/auth_ntlm.c rename to src/libgit2/transports/auth_ntlmclient.c index f49ce101a56..6f26a6179c6 100644 --- a/src/libgit2/transports/auth_ntlm.c +++ b/src/libgit2/transports/auth_ntlmclient.c @@ -23,7 +23,7 @@ typedef struct { bool complete; } http_auth_ntlm_context; -static int ntlm_set_challenge( +static int ntlmclient_set_challenge( git_http_auth_context *c, const char *challenge) { @@ -40,7 +40,7 @@ static int ntlm_set_challenge( return 0; } -static int ntlm_set_credentials(http_auth_ntlm_context *ctx, git_credential *_cred) +static int ntlmclient_set_credentials(http_auth_ntlm_context *ctx, git_credential *_cred) { git_credential_userpass_plaintext *cred; const char *sep, *username; @@ -76,7 +76,7 @@ static int ntlm_set_credentials(http_auth_ntlm_context *ctx, git_credential *_cr return error; } -static int ntlm_next_token( +static int ntlmclient_next_token( git_str *buf, git_http_auth_context *c, git_credential *cred) @@ -104,7 +104,7 @@ static int ntlm_next_token( */ ctx->complete = true; - if (cred && ntlm_set_credentials(ctx, cred) != 0) + if (cred && ntlmclient_set_credentials(ctx, cred) != 0) goto done; if (challenge_len < 4) { @@ -162,7 +162,7 @@ static int ntlm_next_token( return error; } -static int ntlm_is_complete(git_http_auth_context *c) +static int ntlmclient_is_complete(git_http_auth_context *c) { http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; @@ -170,7 +170,7 @@ static int ntlm_is_complete(git_http_auth_context *c) return (ctx->complete == true); } -static void ntlm_context_free(git_http_auth_context *c) +static void ntlmclient_context_free(git_http_auth_context *c) { http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; @@ -179,7 +179,7 @@ static void ntlm_context_free(git_http_auth_context *c) git__free(ctx); } -static int ntlm_init_context( +static int ntlmclient_init_context( http_auth_ntlm_context *ctx, const git_net_url *url) { @@ -206,7 +206,7 @@ int git_http_auth_ntlm( ctx = git__calloc(1, sizeof(http_auth_ntlm_context)); GIT_ERROR_CHECK_ALLOC(ctx); - if (ntlm_init_context(ctx, url) < 0) { + if (ntlmclient_init_context(ctx, url) < 0) { git__free(ctx); return -1; } @@ -214,10 +214,10 @@ int git_http_auth_ntlm( ctx->parent.type = GIT_HTTP_AUTH_NTLM; ctx->parent.credtypes = GIT_CREDENTIAL_USERPASS_PLAINTEXT; ctx->parent.connection_affinity = 1; - ctx->parent.set_challenge = ntlm_set_challenge; - ctx->parent.next_token = ntlm_next_token; - ctx->parent.is_complete = ntlm_is_complete; - ctx->parent.free = ntlm_context_free; + ctx->parent.set_challenge = ntlmclient_set_challenge; + ctx->parent.next_token = ntlmclient_next_token; + ctx->parent.is_complete = ntlmclient_is_complete; + ctx->parent.free = ntlmclient_context_free; *out = (git_http_auth_context *)ctx; From 0d7f3f52918a00a2a07ba965eb65c6ac5d122867 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Sat, 11 Mar 2023 04:45:19 -0800 Subject: [PATCH 3/4] utf8: add conversion with size and refactor names Add functions to use convert a string with length, instead of assuming NUL termination. In addition, move the utf8 to 16 conversion routines into the `git_utf8` namespace instead of using namespaceless `git__` prefixed names. --- src/libgit2/streams/schannel.c | 2 +- src/libgit2/transports/winhttp.c | 24 ++--- src/util/fs_path.c | 2 +- src/util/util.c | 2 +- src/util/win32/error.c | 2 +- src/util/win32/path_w32.c | 16 ++-- src/util/win32/posix_w32.c | 2 +- src/util/win32/utf-conv.c | 148 +++++++++++++++---------------- src/util/win32/utf-conv.h | 95 +++++++++++++++++--- src/util/win32/w32_util.c | 2 +- tests/clar/clar_libgit2.c | 4 +- tests/util/link.c | 6 +- 12 files changed, 185 insertions(+), 120 deletions(-) diff --git a/src/libgit2/streams/schannel.c b/src/libgit2/streams/schannel.c index 2c1066adb7d..f096158193c 100644 --- a/src/libgit2/streams/schannel.c +++ b/src/libgit2/streams/schannel.c @@ -661,7 +661,7 @@ static int schannel_stream_wrap( st->io = in; st->owned = owned; - if (git__utf8_to_16_alloc(&st->host_w, host) < 0) { + if (git_utf8_to_16_alloc(&st->host_w, host) < 0) { git__free(st); return -1; } diff --git a/src/libgit2/transports/winhttp.c b/src/libgit2/transports/winhttp.c index 09822760799..de24a2a41c1 100644 --- a/src/libgit2/transports/winhttp.c +++ b/src/libgit2/transports/winhttp.c @@ -158,10 +158,10 @@ static int apply_userpass_credentials(HINTERNET request, DWORD target, int mecha goto done; } - if ((error = user_len = git__utf8_to_16_alloc(&user, c->username)) < 0) + if ((error = user_len = git_utf8_to_16_alloc(&user, c->username)) < 0) goto done; - if ((error = pass_len = git__utf8_to_16_alloc(&pass, c->password)) < 0) + if ((error = pass_len = git_utf8_to_16_alloc(&pass, c->password)) < 0) goto done; if (!WinHttpSetCredentials(request, target, native_scheme, user, pass, NULL)) { @@ -242,7 +242,7 @@ static int acquire_fallback_cred( HRESULT hCoInitResult; /* Convert URL to wide characters */ - if (git__utf8_to_16_alloc(&wide_url, url) < 0) { + if (git_utf8_to_16_alloc(&wide_url, url) < 0) { git_error_set(GIT_ERROR_OS, "failed to convert string to wide form"); return -1; } @@ -397,7 +397,7 @@ static int winhttp_stream_connect(winhttp_stream *s) return -1; /* Convert URL to wide characters */ - if (git__utf8_to_16_alloc(&s->request_uri, git_str_cstr(&buf)) < 0) { + if (git_utf8_to_16_alloc(&s->request_uri, git_str_cstr(&buf)) < 0) { git_error_set(GIT_ERROR_OS, "failed to convert string to wide form"); goto on_error; } @@ -473,7 +473,7 @@ static int winhttp_stream_connect(winhttp_stream *s) } /* Convert URL to wide characters */ - error = git__utf8_to_16_alloc(&proxy_wide, processed_url.ptr); + error = git_utf8_to_16_alloc(&proxy_wide, processed_url.ptr); git_str_dispose(&processed_url); if (error < 0) goto on_error; @@ -531,7 +531,7 @@ static int winhttp_stream_connect(winhttp_stream *s) s->service) < 0) goto on_error; - if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_str_cstr(&buf)) < 0) { + if (git_utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_str_cstr(&buf)) < 0) { git_error_set(GIT_ERROR_OS, "failed to convert content-type to wide characters"); goto on_error; } @@ -548,7 +548,7 @@ static int winhttp_stream_connect(winhttp_stream *s) s->service) < 0) goto on_error; - if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_str_cstr(&buf)) < 0) { + if (git_utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_str_cstr(&buf)) < 0) { git_error_set(GIT_ERROR_OS, "failed to convert accept header to wide characters"); goto on_error; } @@ -568,7 +568,7 @@ static int winhttp_stream_connect(winhttp_stream *s) git_str_puts(&buf, t->owner->connect_opts.custom_headers.strings[i]); /* Convert header to wide characters */ - if ((error = git__utf8_to_16_alloc(&custom_header_wide, git_str_cstr(&buf))) < 0) + if ((error = git_utf8_to_16_alloc(&custom_header_wide, git_str_cstr(&buf))) < 0) goto on_error; if (!WinHttpAddRequestHeaders(s->request, custom_header_wide, (ULONG)-1L, @@ -783,7 +783,7 @@ static int winhttp_connect( } /* Prepare host */ - if (git__utf8_to_16_alloc(&wide_host, host) < 0) { + if (git_utf8_to_16_alloc(&wide_host, host) < 0) { git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters"); goto on_error; } @@ -792,7 +792,7 @@ static int winhttp_connect( if (git_http__user_agent(&ua) < 0) goto on_error; - if (git__utf8_to_16_alloc(&wide_ua, git_str_cstr(&ua)) < 0) { + if (git_utf8_to_16_alloc(&wide_ua, git_str_cstr(&ua)) < 0) { git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters"); goto on_error; } @@ -1182,7 +1182,7 @@ static int winhttp_stream_read( } /* Convert the Location header to UTF-8 */ - if (git__utf16_to_8_alloc(&location8, location) < 0) { + if (git_utf8_from_16_alloc(&location8, location) < 0) { git_error_set(GIT_ERROR_OS, "failed to convert Location header to UTF-8"); git__free(location); return -1; @@ -1254,7 +1254,7 @@ static int winhttp_stream_read( else p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service); - if (git__utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8) < 0) { + if (git_utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8) < 0) { git_error_set(GIT_ERROR_OS, "failed to convert expected content-type to wide characters"); return -1; } diff --git a/src/util/fs_path.c b/src/util/fs_path.c index b52867e779f..e03fcf7c760 100644 --- a/src/util/fs_path.c +++ b/src/util/fs_path.c @@ -2015,7 +2015,7 @@ int git_fs_path_find_executable(git_str *fullpath, const char *executable) git_win32_path fullpath_w, executable_w; int error; - if (git__utf8_to_16(executable_w, GIT_WIN_PATH_MAX, executable) < 0) + if (git_utf8_to_16(executable_w, GIT_WIN_PATH_MAX, executable) < 0) return -1; error = git_win32_path_find_executable(fullpath_w, executable_w); diff --git a/src/util/util.c b/src/util/util.c index aee95fddfb5..9c9f2c0403a 100644 --- a/src/util/util.c +++ b/src/util/util.c @@ -743,7 +743,7 @@ int git__getenv(git_str *out, const char *name) git_str_clear(out); - if (git__utf8_to_16_alloc(&wide_name, name) < 0) + if (git_utf8_to_16_alloc(&wide_name, name) < 0) return -1; if ((value_len = GetEnvironmentVariableW(wide_name, NULL, 0)) > 0) { diff --git a/src/util/win32/error.c b/src/util/win32/error.c index 3a52fb5a90a..dfd6fa1e8a1 100644 --- a/src/util/win32/error.c +++ b/src/util/win32/error.c @@ -43,7 +43,7 @@ char *git_win32_get_error_message(DWORD error_code) (LPWSTR)&lpMsgBuf, 0, NULL)) { /* Convert the message to UTF-8. If this fails, we will * return NULL, which is a condition expected by the caller */ - if (git__utf16_to_8_alloc(&utf8_msg, lpMsgBuf) < 0) + if (git_utf8_from_16_alloc(&utf8_msg, lpMsgBuf) < 0) utf8_msg = NULL; LocalFree(lpMsgBuf); diff --git a/src/util/win32/path_w32.c b/src/util/win32/path_w32.c index d9fc8292b0c..7a559e45c58 100644 --- a/src/util/win32/path_w32.c +++ b/src/util/win32/path_w32.c @@ -336,13 +336,13 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src) /* See if this is an absolute path (beginning with a drive letter) */ if (git_fs_path_is_absolute(src)) { - if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src) < 0) + if (git_utf8_to_16(dest, GIT_WIN_PATH_MAX, src) < 0) goto on_error; } /* File-prefixed NT-style paths beginning with \\?\ */ else if (path__is_nt_namespace(src)) { /* Skip the NT prefix, the destination already contains it */ - if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src + PATH__NT_NAMESPACE_LEN) < 0) + if (git_utf8_to_16(dest, GIT_WIN_PATH_MAX, src + PATH__NT_NAMESPACE_LEN) < 0) goto on_error; } /* UNC paths */ @@ -351,7 +351,7 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src) dest += 4; /* Skip the leading "\\" */ - if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX - 2, src + 2) < 0) + if (git_utf8_to_16(dest, GIT_WIN_PATH_MAX - 2, src + 2) < 0) goto on_error; } /* Absolute paths omitting the drive letter */ @@ -365,7 +365,7 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src) } /* Skip the drive letter specification ("C:") */ - if (git__utf8_to_16(dest + 2, GIT_WIN_PATH_MAX - 2, src) < 0) + if (git_utf8_to_16(dest + 2, GIT_WIN_PATH_MAX - 2, src) < 0) goto on_error; } /* Relative paths */ @@ -377,7 +377,7 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src) dest[cwd_len++] = L'\\'; - if (git__utf8_to_16(dest + cwd_len, GIT_WIN_PATH_MAX - cwd_len, src) < 0) + if (git_utf8_to_16(dest + cwd_len, GIT_WIN_PATH_MAX - cwd_len, src) < 0) goto on_error; } @@ -404,7 +404,7 @@ int git_win32_path_relative_from_utf8(git_win32_path out, const char *src) return git_win32_path_from_utf8(out, src); } - if ((len = git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src)) < 0) + if ((len = git_utf8_to_16(dest, GIT_WIN_PATH_MAX, src)) < 0) return -1; for (p = dest; p < (dest + len); p++) { @@ -433,7 +433,7 @@ int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src) } } - if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0) + if ((len = git_utf8_from_16(out, GIT_WIN_PATH_UTF8, src)) < 0) return len; git_fs_path_mkposix(dest); @@ -471,7 +471,7 @@ char *git_win32_path_8dot3_name(const char *path) if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL) return NULL; - if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0) + if ((len = git_utf8_from_16(shortname, namelen + 1, start)) < 0) return NULL; return shortname; diff --git a/src/util/win32/posix_w32.c b/src/util/win32/posix_w32.c index 5862e5c9ad6..3fec469a648 100644 --- a/src/util/win32/posix_w32.c +++ b/src/util/win32/posix_w32.c @@ -649,7 +649,7 @@ int p_getcwd(char *buffer_out, size_t size) git_win32_path_remove_namespace(cwd, wcslen(cwd)); /* Convert the working directory back to UTF-8 */ - if (git__utf16_to_8(buffer_out, size, cwd) < 0) { + if (git_utf8_from_16(buffer_out, size, cwd) < 0) { DWORD code = GetLastError(); if (code == ERROR_INSUFFICIENT_BUFFER) diff --git a/src/util/win32/utf-conv.c b/src/util/win32/utf-conv.c index 4bde3023ab6..ad35c0c35ff 100644 --- a/src/util/win32/utf-conv.c +++ b/src/util/win32/utf-conv.c @@ -15,108 +15,114 @@ GIT_INLINE(void) git__set_errno(void) errno = EINVAL; } -/** - * Converts a UTF-8 string to wide characters. - * - * @param dest The buffer to receive the wide string. - * @param dest_size The size of the buffer, in characters. - * @param src The UTF-8 string to convert. - * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure - */ -int git__utf8_to_16(wchar_t *dest, size_t dest_size, const char *src) +int git_utf8_to_16(wchar_t *dest, size_t dest_size, const char *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_to_16_with_len(dest, dest_size, src, -1); +} + +int git_utf8_to_16_with_len( + wchar_t *dest, + size_t _dest_size, + const char *src, + int src_len) { + int dest_size = (int)min(_dest_size, INT_MAX); int len; - /* Length of -1 indicates NULL termination of the input string. Subtract 1 from the result to - * turn 0 into -1 (an error code) and to not count the NULL terminator as part of the string's - * length. MultiByteToWideChar never returns int's minvalue, so underflow is not possible */ - if ((len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, dest, (int)dest_size) - 1) < 0) + /* + * Subtract 1 from the result to turn 0 into -1 (an error code) and + * to not count the NULL terminator as part of the string's length. + * MultiByteToWideChar never returns int's minvalue, so underflow + * is not possible. + */ + len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + src, src_len, dest, dest_size) - 1; + + if (len < 0) git__set_errno(); return len; } -/** - * Converts a wide string to UTF-8. - * - * @param dest The buffer to receive the UTF-8 string. - * @param dest_size The size of the buffer, in bytes. - * @param src The wide string to convert. - * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure - */ -int git__utf16_to_8(char *dest, size_t dest_size, const wchar_t *src) +int git_utf8_from_16(char *dest, size_t dest_size, const wchar_t *src) { + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_from_16_with_len(dest, dest_size, src, -1); +} + +int git_utf8_from_16_with_len( + char *dest, + size_t _dest_size, + const wchar_t *src, + int src_len) +{ + int dest_size = (int)min(_dest_size, INT_MAX); int len; - /* Length of -1 indicates NULL termination of the input string. Subtract 1 from the result to - * turn 0 into -1 (an error code) and to not count the NULL terminator as part of the string's - * length. WideCharToMultiByte never returns int's minvalue, so underflow is not possible */ - if ((len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, dest, (int)dest_size, NULL, NULL) - 1) < 0) + /* + * Subtract 1 from the result to turn 0 into -1 (an error code) and + * to not count the NULL terminator as part of the string's length. + * WideCharToMultiByte never returns int's minvalue, so underflow + * is not possible. + */ + len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + src, src_len, dest, dest_size, NULL, NULL) - 1; + + if (len < 0) git__set_errno(); return len; } -/** - * Converts a UTF-8 string to wide characters. - * Memory is allocated to hold the converted string. - * The caller is responsible for freeing the string with git__free. - * - * @param dest Receives a pointer to the wide string. - * @param src The UTF-8 string to convert. - * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure - */ -int git__utf8_to_16_alloc(wchar_t **dest, const char *src) +int git_utf8_to_16_alloc(wchar_t **dest, const char *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_to_16_alloc_with_len(dest, src, -1); +} + +int git_utf8_to_16_alloc_with_len(wchar_t **dest, const char *src, int src_len) { int utf16_size; *dest = NULL; - /* Length of -1 indicates NULL termination of the input string */ - utf16_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, NULL, 0); + utf16_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + src, src_len, NULL, 0); if (!utf16_size) { git__set_errno(); return -1; } - if (!(*dest = git__mallocarray(utf16_size, sizeof(wchar_t)))) { - errno = ENOMEM; - return -1; - } + *dest = git__mallocarray(utf16_size, sizeof(wchar_t)); + GIT_ERROR_CHECK_ALLOC(*dest); - utf16_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, *dest, utf16_size); - - if (!utf16_size) { - git__set_errno(); + utf16_size = git_utf8_to_16_with_len(*dest, (size_t)utf16_size, + src, src_len); + if (utf16_size < 0) { git__free(*dest); *dest = NULL; } - /* Subtract 1 from the result to turn 0 into -1 (an error code) and to not count the NULL - * terminator as part of the string's length. MultiByteToWideChar never returns int's minvalue, - * so underflow is not possible */ - return utf16_size - 1; + return utf16_size; } -/** - * Converts a wide string to UTF-8. - * Memory is allocated to hold the converted string. - * The caller is responsible for freeing the string with git__free. - * - * @param dest Receives a pointer to the UTF-8 string. - * @param src The wide string to convert. - * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure - */ -int git__utf16_to_8_alloc(char **dest, const wchar_t *src) +int git_utf8_from_16_alloc(char **dest, const wchar_t *src) +{ + /* Length of -1 indicates NULL termination of the input string. */ + return git_utf8_from_16_alloc_with_len(dest, src, -1); +} + +int git_utf8_from_16_alloc_with_len(char **dest, const wchar_t *src, int src_len) { int utf8_size; *dest = NULL; - /* Length of -1 indicates NULL termination of the input string */ - utf8_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, NULL, 0, NULL, NULL); + utf8_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + src, src_len, NULL, 0, NULL, NULL); if (!utf8_size) { git__set_errno(); @@ -124,23 +130,15 @@ int git__utf16_to_8_alloc(char **dest, const wchar_t *src) } *dest = git__malloc(utf8_size); + GIT_ERROR_CHECK_ALLOC(*dest); - if (!*dest) { - errno = ENOMEM; - return -1; - } - - utf8_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, *dest, utf8_size, NULL, NULL); - - if (!utf8_size) { - git__set_errno(); + utf8_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + src, src_len, *dest, utf8_size, NULL, NULL); + if (utf8_size < 0) { git__free(*dest); *dest = NULL; } - /* Subtract 1 from the result to turn 0 into -1 (an error code) and to not count the NULL - * terminator as part of the string's length. MultiByteToWideChar never returns int's minvalue, - * so underflow is not possible */ - return utf8_size - 1; + return utf8_size; } diff --git a/src/util/win32/utf-conv.h b/src/util/win32/utf-conv.h index 120d647efdf..301f5a6d36c 100644 --- a/src/util/win32/utf-conv.h +++ b/src/util/win32/utf-conv.h @@ -15,15 +15,46 @@ # define WC_ERR_INVALID_CHARS 0x80 #endif +/** + * Converts a NUL-terminated UTF-8 string to wide characters. This is a + * convenience function for `git_utf8_to_16_with_len`. + * + * @param dest The buffer to receive the wide string. + * @param dest_size The size of the buffer, in characters. + * @param src The UTF-8 string to convert. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16(wchar_t *dest, size_t dest_size, const char *src); + /** * Converts a UTF-8 string to wide characters. * * @param dest The buffer to receive the wide string. * @param dest_size The size of the buffer, in characters. * @param src The UTF-8 string to convert. - * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure + * @param src_len The length of the string to convert. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16_with_len( + wchar_t *dest, + size_t dest_size, + const char *src, + int src_len); + +/** + * Converts a NUL-terminated wide string to UTF-8. This is a convenience + * function for `git_utf8_from_16_with_len`. + * + * @param dest The buffer to receive the UTF-8 string. + * @param dest_size The size of the buffer, in bytes. + * @param src The wide string to convert. + * @param src_len The length of the string to convert. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure */ -int git__utf8_to_16(wchar_t *dest, size_t dest_size, const char *src); +int git_utf8_from_16(char *dest, size_t dest_size, const wchar_t *src); /** * Converts a wide string to UTF-8. @@ -31,30 +62,66 @@ int git__utf8_to_16(wchar_t *dest, size_t dest_size, const char *src); * @param dest The buffer to receive the UTF-8 string. * @param dest_size The size of the buffer, in bytes. * @param src The wide string to convert. - * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure + * @param src_len The length of the string to convert. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure */ -int git__utf16_to_8(char *dest, size_t dest_size, const wchar_t *src); +int git_utf8_from_16_with_len(char *dest, size_t dest_size, const wchar_t *src, int src_len); /** - * Converts a UTF-8 string to wide characters. - * Memory is allocated to hold the converted string. - * The caller is responsible for freeing the string with git__free. + * Converts a UTF-8 string to wide characters. Memory is allocated to hold + * the converted string. The caller is responsible for freeing the string + * with git__free. * * @param dest Receives a pointer to the wide string. * @param src The UTF-8 string to convert. - * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure */ -int git__utf8_to_16_alloc(wchar_t **dest, const char *src); +int git_utf8_to_16_alloc(wchar_t **dest, const char *src); /** - * Converts a wide string to UTF-8. - * Memory is allocated to hold the converted string. - * The caller is responsible for freeing the string with git__free. + * Converts a UTF-8 string to wide characters. Memory is allocated to hold + * the converted string. The caller is responsible for freeing the string + * with git__free. + * + * @param dest Receives a pointer to the wide string. + * @param src The UTF-8 string to convert. + * @param src_len The length of the string. + * @return The length of the wide string, in characters + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_to_16_alloc_with_len( + wchar_t **dest, + const char *src, + int src_len); + +/** + * Converts a wide string to UTF-8. Memory is allocated to hold the + * converted string. The caller is responsible for freeing the string + * with git__free. + * + * @param dest Receives a pointer to the UTF-8 string. + * @param src The wide string to convert. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure + */ +int git_utf8_from_16_alloc(char **dest, const wchar_t *src); + +/** + * Converts a wide string to UTF-8. Memory is allocated to hold the + * converted string. The caller is responsible for freeing the string + * with git__free. * * @param dest Receives a pointer to the UTF-8 string. * @param src The wide string to convert. - * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure + * @param src_len The length of the wide string. + * @return The length of the UTF-8 string, in bytes + * (not counting the NULL terminator), or < 0 for failure */ -int git__utf16_to_8_alloc(char **dest, const wchar_t *src); +int git_utf8_from_16_alloc_with_len( + char **dest, + const wchar_t *src, + int src_len); #endif diff --git a/src/util/win32/w32_util.c b/src/util/win32/w32_util.c index fe4b75baefd..f5b006a1974 100644 --- a/src/util/win32/w32_util.c +++ b/src/util/win32/w32_util.c @@ -115,7 +115,7 @@ int git_win32__file_attribute_to_stat( /* st_size gets the UTF-8 length of the target name, in bytes, * not counting the NULL terminator */ - if ((st->st_size = git__utf16_to_8(NULL, 0, target)) < 0) { + if ((st->st_size = git_utf8_from_16(NULL, 0, target)) < 0) { git_error_set(GIT_ERROR_OS, "could not convert reparse point name for '%ls'", path); return -1; } diff --git a/tests/clar/clar_libgit2.c b/tests/clar/clar_libgit2.c index 54122997df7..a1b92fc336e 100644 --- a/tests/clar/clar_libgit2.c +++ b/tests/clar/clar_libgit2.c @@ -103,10 +103,10 @@ int cl_setenv(const char *name, const char *value) { wchar_t *wide_name, *wide_value = NULL; - cl_assert(git__utf8_to_16_alloc(&wide_name, name) >= 0); + cl_assert(git_utf8_to_16_alloc(&wide_name, name) >= 0); if (value) { - cl_assert(git__utf8_to_16_alloc(&wide_value, value) >= 0); + cl_assert(git_utf8_to_16_alloc(&wide_value, value) >= 0); cl_assert(SetEnvironmentVariableW(wide_name, wide_value)); } else { /* Windows XP returns 0 (failed) when passing NULL for lpValue when diff --git a/tests/util/link.c b/tests/util/link.c index 46cafada7e9..5909e26e391 100644 --- a/tests/util/link.c +++ b/tests/util/link.c @@ -98,7 +98,7 @@ static void do_junction(const char *old, const char *new) git_str_putc(&unparsed_buf, '\\'); - subst_utf16_len = git__utf8_to_16(NULL, 0, git_str_cstr(&unparsed_buf)); + subst_utf16_len = git_utf8_to_16(NULL, 0, git_str_cstr(&unparsed_buf)); subst_byte_len = subst_utf16_len * sizeof(WCHAR); print_utf16_len = subst_utf16_len - 4; @@ -124,11 +124,11 @@ static void do_junction(const char *old, const char *new) subst_utf16 = reparse_buf->ReparseBuffer.MountPoint.PathBuffer; print_utf16 = subst_utf16 + subst_utf16_len + 1; - ret = git__utf8_to_16(subst_utf16, subst_utf16_len + 1, + ret = git_utf8_to_16(subst_utf16, subst_utf16_len + 1, git_str_cstr(&unparsed_buf)); cl_assert_equal_i(subst_utf16_len, ret); - ret = git__utf8_to_16(print_utf16, + ret = git_utf8_to_16(print_utf16, print_utf16_len + 1, git_str_cstr(&unparsed_buf) + 4); cl_assert_equal_i(print_utf16_len, ret); From f15c8ac71a916bf186cd5ff81f07ca85eef82afb Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 13 Mar 2023 07:08:46 -0700 Subject: [PATCH 4/4] http: add SSPI authentication on Windows Add support for SSPI on Windows, which offers NTLM and Negotiate authentication. --- cmake/SelectGSSAPI.cmake | 6 +- cmake/SelectHTTPSBackend.cmake | 4 +- src/libgit2/transports/auth_negotiate.h | 2 +- src/libgit2/transports/auth_ntlm.h | 6 +- src/libgit2/transports/auth_sspi.c | 341 ++++++++++++++++++++++++ 5 files changed, 350 insertions(+), 9 deletions(-) create mode 100644 src/libgit2/transports/auth_sspi.c diff --git a/cmake/SelectGSSAPI.cmake b/cmake/SelectGSSAPI.cmake index 24e2d68b9a7..5bde11697df 100644 --- a/cmake/SelectGSSAPI.cmake +++ b/cmake/SelectGSSAPI.cmake @@ -29,7 +29,7 @@ if(USE_GSSAPI) list(APPEND LIBGIT2_SYSTEM_LIBS ${GSSFRAMEWORK_LIBRARIES}) set(GIT_GSSFRAMEWORK 1) - add_feature_info(SPNEGO GIT_GSSFRAMEWORK "SPNEGO authentication support (${USE_GSSAPI})") + add_feature_info(GSSAPI GIT_GSSFRAMEWORK "GSSAPI support for SPNEGO authentication (${USE_GSSAPI})") elseif(USE_GSSAPI STREQUAL "gssapi") if(NOT GSSAPI_FOUND) message(FATAL_ERROR "Asked for gssapi GSS backend, but it wasn't found") @@ -38,11 +38,11 @@ if(USE_GSSAPI) list(APPEND LIBGIT2_SYSTEM_LIBS ${GSSAPI_LIBRARIES}) set(GIT_GSSAPI 1) - add_feature_info(SPNEGO GIT_GSSAPI "SPNEGO authentication support (${USE_GSSAPI})") + add_feature_info(GSSAPI GIT_GSSAPI "GSSAPI support for SPNEGO authentication (${USE_GSSAPI})") else() message(FATAL_ERROR "Asked for backend ${USE_GSSAPI} but it wasn't found") endif() else() set(GIT_GSSAPI 0) - add_feature_info(SPNEGO NO "SPNEGO authentication support") + add_feature_info(GSSAPI NO "GSSAPI support for SPNEGO authentication") endif() diff --git a/cmake/SelectHTTPSBackend.cmake b/cmake/SelectHTTPSBackend.cmake index 64c7a1097ce..d14941643dc 100644 --- a/cmake/SelectHTTPSBackend.cmake +++ b/cmake/SelectHTTPSBackend.cmake @@ -125,8 +125,8 @@ if(USE_HTTPS) list(APPEND LIBGIT2_PC_LIBS "-lwinhttp") endif() - list(APPEND LIBGIT2_SYSTEM_LIBS "rpcrt4" "crypt32" "ole32") - list(APPEND LIBGIT2_PC_LIBS "-lrpcrt4" "-lcrypt32" "-lole32") + list(APPEND LIBGIT2_SYSTEM_LIBS "rpcrt4" "crypt32" "ole32" "secur32") + list(APPEND LIBGIT2_PC_LIBS "-lrpcrt4" "-lcrypt32" "-lole32" "-lsecur32") elseif(USE_HTTPS STREQUAL "OpenSSL-Dynamic") set(GIT_OPENSSL 1) set(GIT_OPENSSL_DYNAMIC 1) diff --git a/src/libgit2/transports/auth_negotiate.h b/src/libgit2/transports/auth_negotiate.h index 34aff295b13..4360785c555 100644 --- a/src/libgit2/transports/auth_negotiate.h +++ b/src/libgit2/transports/auth_negotiate.h @@ -12,7 +12,7 @@ #include "git2.h" #include "auth.h" -#if defined(GIT_GSSAPI) || defined(GIT_GSSFRAMEWORK) +#if defined(GIT_GSSAPI) || defined(GIT_GSSFRAMEWORK) || defined(GIT_WIN32) extern int git_http_auth_negotiate( git_http_auth_context **out, diff --git a/src/libgit2/transports/auth_ntlm.h b/src/libgit2/transports/auth_ntlm.h index 2bf8f41f619..33406ae94c3 100644 --- a/src/libgit2/transports/auth_ntlm.h +++ b/src/libgit2/transports/auth_ntlm.h @@ -5,15 +5,15 @@ * a Linking Exception. For full terms see the included COPYING file. */ -#ifndef INCLUDE_transports_auth_ntlmclient_h__ -#define INCLUDE_transports_auth_ntlmclient_h__ +#ifndef INCLUDE_transports_auth_ntlm_h__ +#define INCLUDE_transports_auth_ntlm_h__ #include "auth.h" /* NTLM requires a full request/challenge/response */ #define GIT_AUTH_STEPS_NTLM 2 -#ifdef GIT_NTLM +#if defined(GIT_NTLM) || defined(GIT_WIN32) #if defined(GIT_OPENSSL) # define CRYPT_OPENSSL diff --git a/src/libgit2/transports/auth_sspi.c b/src/libgit2/transports/auth_sspi.c new file mode 100644 index 00000000000..f8269365d7f --- /dev/null +++ b/src/libgit2/transports/auth_sspi.c @@ -0,0 +1,341 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "auth_ntlm.h" +#include "auth_negotiate.h" + +#ifdef GIT_WIN32 + +#define SECURITY_WIN32 + +#include "git2.h" +#include "auth.h" +#include "git2/sys/credential.h" + +#include +#include + +typedef struct { + git_http_auth_context parent; + wchar_t *target; + + const char *package_name; + size_t package_name_len; + wchar_t *package_name_w; + SecPkgInfoW *package_info; + SEC_WINNT_AUTH_IDENTITY_W identity; + CredHandle cred; + CtxtHandle context; + + int has_identity : 1, + has_credentials : 1, + has_context : 1, + complete : 1; + git_str challenge; +} http_auth_sspi_context; + +static void sspi_reset_context(http_auth_sspi_context *ctx) +{ + if (ctx->has_identity) { + git__free(ctx->identity.User); + git__free(ctx->identity.Domain); + git__free(ctx->identity.Password); + + memset(&ctx->identity, 0, sizeof(SEC_WINNT_AUTH_IDENTITY_W)); + + ctx->has_identity = 0; + } + + if (ctx->has_credentials) { + FreeCredentialsHandle(&ctx->cred); + memset(&ctx->cred, 0, sizeof(CredHandle)); + + ctx->has_credentials = 0; + } + + if (ctx->has_context) { + DeleteSecurityContext(&ctx->context); + memset(&ctx->context, 0, sizeof(CtxtHandle)); + + ctx->has_context = 0; + } + + ctx->complete = 0; + + git_str_dispose(&ctx->challenge); +} + +static int sspi_set_challenge( + git_http_auth_context *c, + const char *challenge) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + size_t challenge_len = strlen(challenge); + + git_str_clear(&ctx->challenge); + + if (strncmp(challenge, ctx->package_name, ctx->package_name_len) != 0) { + git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name); + return -1; + } + + /* + * A package type indicator without a base64 payload indicates the + * mechanism; it's not an actual challenge. Ignore it. + */ + if (challenge[ctx->package_name_len] == 0) { + return 0; + } else if (challenge[ctx->package_name_len] != ' ') { + git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name); + return -1; + } + + if (git_str_decode_base64(&ctx->challenge, + challenge + (ctx->package_name_len + 1), + challenge_len - (ctx->package_name_len + 1)) < 0) { + git_error_set(GIT_ERROR_NET, "invalid %s challenge from server", ctx->package_name); + return -1; + } + + GIT_ASSERT(ctx->challenge.size <= ULONG_MAX); + return 0; +} + +static int create_identity( + SEC_WINNT_AUTH_IDENTITY_W **out, + http_auth_sspi_context *ctx, + git_credential *cred) +{ + git_credential_userpass_plaintext *userpass; + wchar_t *username = NULL, *domain = NULL, *password = NULL; + int username_len = 0, domain_len = 0, password_len = 0; + const char *sep; + + if (cred->credtype == GIT_CREDENTIAL_DEFAULT) { + *out = NULL; + return 0; + } + + if (cred->credtype != GIT_CREDENTIAL_USERPASS_PLAINTEXT) { + git_error_set(GIT_ERROR_NET, "unknown credential type: %d", cred->credtype); + return -1; + } + + userpass = (git_credential_userpass_plaintext *)cred; + + if ((sep = strchr(userpass->username, '\\')) != NULL) { + GIT_ASSERT(sep - userpass->username < INT_MAX); + + username_len = git_utf8_to_16_alloc(&username, sep + 1); + domain_len = git_utf8_to_16_alloc_with_len(&domain, + userpass->username, (int)(sep - userpass->username)); + } else { + username_len = git_utf8_to_16_alloc(&username, + userpass->username); + } + + password_len = git_utf8_to_16_alloc(&password, userpass->password); + + if (username_len < 0 || domain_len < 0 || password_len < 0) { + git__free(username); + git__free(domain); + git__free(password); + return -1; + } + + ctx->identity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; + ctx->identity.User = username; + ctx->identity.UserLength = (unsigned long)username_len; + ctx->identity.Password = password; + ctx->identity.PasswordLength = (unsigned long)password_len; + ctx->identity.Domain = domain; + ctx->identity.DomainLength = (unsigned long)domain_len; + + ctx->has_identity = 1; + + *out = &ctx->identity; + + return 0; +} + +static int sspi_next_token( + git_str *buf, + git_http_auth_context *c, + git_credential *cred) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + SEC_WINNT_AUTH_IDENTITY_W *identity = NULL; + TimeStamp timestamp; + DWORD context_flags; + SecBuffer input_buf = { 0, SECBUFFER_TOKEN, NULL }; + SecBuffer output_buf = { 0, SECBUFFER_TOKEN, NULL }; + SecBufferDesc input_buf_desc = { SECBUFFER_VERSION, 1, &input_buf }; + SecBufferDesc output_buf_desc = { SECBUFFER_VERSION, 1, &output_buf }; + SECURITY_STATUS status; + + if (ctx->complete) + sspi_reset_context(ctx); + + if (!ctx->has_context) { + if (create_identity(&identity, ctx, cred) < 0) + return -1; + + status = AcquireCredentialsHandleW(NULL, ctx->package_name_w, + SECPKG_CRED_BOTH, NULL, identity, NULL, + NULL, &ctx->cred, ×tamp); + + if (status != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not acquire credentials"); + return -1; + } + + ctx->has_credentials = 1; + } + + context_flags = ISC_REQ_ALLOCATE_MEMORY | + ISC_REQ_CONFIDENTIALITY | + ISC_REQ_MUTUAL_AUTH; + + if (ctx->challenge.size > 0) { + input_buf.BufferType = SECBUFFER_TOKEN; + input_buf.cbBuffer = (unsigned long)ctx->challenge.size; + input_buf.pvBuffer = ctx->challenge.ptr; + } + + status = InitializeSecurityContextW(&ctx->cred, + ctx->has_context ? &ctx->context : NULL, + ctx->target, + context_flags, + 0, + SECURITY_NETWORK_DREP, + ctx->has_context ? &input_buf_desc : NULL, + 0, + ctx->has_context ? NULL : &ctx->context, + &output_buf_desc, + &context_flags, + NULL); + + if (status == SEC_I_COMPLETE_AND_CONTINUE || + status == SEC_I_COMPLETE_NEEDED) + status = CompleteAuthToken(&ctx->context, &output_buf_desc); + + if (status == SEC_E_OK) { + ctx->complete = 1; + } else if (status != SEC_I_CONTINUE_NEEDED) { + git_error_set(GIT_ERROR_OS, "could not initialize security context"); + return -1; + } + + ctx->has_context = 1; + git_str_clear(&ctx->challenge); + + if (output_buf.cbBuffer > 0) { + git_str_put(buf, ctx->package_name, ctx->package_name_len); + git_str_putc(buf, ' '); + git_str_encode_base64(buf, output_buf.pvBuffer, output_buf.cbBuffer); + + FreeContextBuffer(output_buf.pvBuffer); + + if (git_str_oom(buf)) + return -1; + } + + return 0; +} + +static int sspi_is_complete(git_http_auth_context *c) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + + return ctx->complete; +} + +static void sspi_context_free(git_http_auth_context *c) +{ + http_auth_sspi_context *ctx = (http_auth_sspi_context *)c; + + sspi_reset_context(ctx); + + FreeContextBuffer(ctx->package_info); + git__free(ctx->target); + git__free(ctx); +} + +static int sspi_init_context( + git_http_auth_context **out, + git_http_auth_t type, + const git_net_url *url) +{ + http_auth_sspi_context *ctx; + git_str target = GIT_STR_INIT; + + *out = NULL; + + ctx = git__calloc(1, sizeof(http_auth_sspi_context)); + GIT_ERROR_CHECK_ALLOC(ctx); + + switch (type) { + case GIT_HTTP_AUTH_NTLM: + ctx->package_name = "NTLM"; + ctx->package_name_len = CONST_STRLEN("NTLM"); + ctx->package_name_w = L"NTLM"; + ctx->parent.credtypes = GIT_CREDENTIAL_USERPASS_PLAINTEXT | + GIT_CREDENTIAL_DEFAULT; + break; + case GIT_HTTP_AUTH_NEGOTIATE: + ctx->package_name = "Negotiate"; + ctx->package_name_len = CONST_STRLEN("Negotiate"); + ctx->package_name_w = L"Negotiate"; + ctx->parent.credtypes = GIT_CREDENTIAL_DEFAULT; + break; + default: + git_error_set(GIT_ERROR_NET, "unknown SSPI auth type: %d", ctx->parent.type); + git__free(ctx); + return -1; + } + + if (QuerySecurityPackageInfoW(ctx->package_name_w, &ctx->package_info) != SEC_E_OK) { + git_error_set(GIT_ERROR_OS, "could not query security package"); + git__free(ctx); + return -1; + } + + if (git_str_printf(&target, "http/%s", url->host) < 0 || + git_utf8_to_16_alloc(&ctx->target, target.ptr) < 0) { + FreeContextBuffer(ctx->package_info); + git__free(ctx); + return -1; + } + + ctx->parent.type = type; + ctx->parent.connection_affinity = 1; + ctx->parent.set_challenge = sspi_set_challenge; + ctx->parent.next_token = sspi_next_token; + ctx->parent.is_complete = sspi_is_complete; + ctx->parent.free = sspi_context_free; + + *out = (git_http_auth_context *)ctx; + + git_str_dispose(&target); + return 0; +} + +int git_http_auth_negotiate( + git_http_auth_context **out, + const git_net_url *url) +{ + return sspi_init_context(out, GIT_HTTP_AUTH_NEGOTIATE, url); +} + +int git_http_auth_ntlm( + git_http_auth_context **out, + const git_net_url *url) +{ + return sspi_init_context(out, GIT_HTTP_AUTH_NTLM, url); +} + +#endif /* GIT_WIN32 */