Implement mTLS support in WinHTTP transport. (#6131)

* Very preliminary mTLS implementation

* Tests for TLS client certificate

* Tested mTLS functionality

* Added changelog entry; updated PCCERT_CONTEXT using declaration to be more succinct.
This commit is contained in:
Larry Osterman 2024-11-06 12:03:08 -08:00 committed by GitHub
parent 3687136d04
commit 9770fb77dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1224 additions and 770 deletions

2
.vscode/cspell.json vendored
View File

@ -207,6 +207,8 @@
"opentelemetry",
"Osterman",
"otel",
"PCCERT",
"PCERT",
"PBYTE",
"pdbs",
"phoebusm",

View File

@ -4,6 +4,9 @@
### Features Added
- Added mTLS support to WinHTTP transport.
- To enable mTLS, first create an appropriate Windows `PCCERT_CONTEXT` object and set the `TlsClientCertificate` field in `WinHttpTransportOptions` to that certificate before creating the `WinHttpTransport` object.
### Breaking Changes
### Bugs Fixed

View File

@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// cspell: words PCCERT
/**
* @file
* @brief #Azure::Core::Http::HttpTransport implementation via WinHTTP.
@ -20,10 +22,16 @@
#include <string>
#include <vector>
/**
* @brief Declaration of a Windows PCCERT_CONTEXT structure from the Windows SDK.
*/
using PCCERT_CONTEXT = const struct _CERT_CONTEXT*;
namespace Azure { namespace Core { namespace Http {
namespace _detail {
class WinHttpTransportImpl;
class WinHttpRequest;
}
} // namespace _detail
/**
* @brief Sets the WinHTTP session and connection options used to customize the behavior of the
@ -91,6 +99,12 @@ namespace Azure { namespace Core { namespace Http {
* the server.
*/
std::vector<std::string> ExpectedTlsRootCertificates;
/**
* @brief TLS Client Certificate Context, used when the TLS Server requests mTLS client
* authentication.
*/
PCCERT_CONTEXT TlsClientCertificate{nullptr};
};
/**
@ -99,23 +113,16 @@ namespace Azure { namespace Core { namespace Http {
*/
class WinHttpTransport : public HttpTransport {
private:
WinHttpTransportOptions m_options;
// m_sessionhandle is const to ensure immutability.
const Azure::Core::_internal::UniqueHandle<void*> m_sessionHandle;
std::unique_ptr<_detail::WinHttpTransportImpl> m_impl;
Azure::Core::_internal::UniqueHandle<void*> CreateSessionHandle();
Azure::Core::_internal::UniqueHandle<void*> CreateConnectionHandle(
Azure::Core::Url const& url,
Azure::Core::Context const& context);
std::unique_ptr<_detail::WinHttpRequest> CreateRequestHandle(
Azure::Core::_internal::UniqueHandle<void*> const& connectionHandle,
Azure::Core::Url const& url,
Azure::Core::Http::HttpMethod const& method);
// Callback to allow a derived transport to extract the request handle. Used for WebSocket
// transports.
virtual void OnUpgradedConnection(std::unique_ptr<_detail::WinHttpRequest> const&){};
protected:
/** @brief Callback to allow a derived transport to extract the request handle. Used for
* WebSocket transports.
*
* @param request - Request which contains the WinHttp request handle.
*/
virtual void OnUpgradedConnection(
std::unique_ptr<_detail::WinHttpRequest> const& request) const;
public:
/**
@ -125,11 +132,6 @@ namespace Azure { namespace Core { namespace Http {
*/
WinHttpTransport(WinHttpTransportOptions const& options = WinHttpTransportOptions());
/**
* @brief Constructs `%WinHttpTransport`.
*
* @param options Optional parameter to override the default settings.
*/
/**
* @brief Constructs `%WinHttpTransport` object based on common Azure HTTP Transport Options
*
@ -148,6 +150,10 @@ namespace Azure { namespace Core { namespace Http {
// and virtual or protected and
// non-virtual"](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c35-a-base-class-destructor-should-be-either-public-and-virtual-or-protected-and-non-virtual)
virtual ~WinHttpTransport();
// @cond
friend _detail::WinHttpTransportImpl;
// @endcond
};
}}} // namespace Azure::Core::Http

View File

@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include <memory>
#include <wincrypt.h>
#include <wil/resource.h>
namespace Azure { namespace Core { namespace Http { namespace _detail {
class WinHttpTransportImpl : HttpTransport {
private:
WinHttpTransport const* m_parent;
WinHttpTransportOptions m_options;
// m_sessionhandle is const to ensure immutability.
const Azure::Core::_internal::UniqueHandle<void*> m_sessionHandle;
wil::unique_cert_context m_tlsClientCertificate;
Azure::Core::_internal::UniqueHandle<void*> CreateSessionHandle();
Azure::Core::_internal::UniqueHandle<void*> CreateConnectionHandle(
Azure::Core::Url const& url,
Azure::Core::Context const& context);
std::unique_ptr<_detail::WinHttpRequest> CreateRequestHandle(
Azure::Core::_internal::UniqueHandle<void*> const& connectionHandle,
Azure::Core::Url const& url,
Azure::Core::Http::HttpMethod const& method);
// Callback to allow a derived transport to extract the request handle. Used for WebSocket
// transports.
virtual void OnUpgradedConnection(std::unique_ptr<_detail::WinHttpRequest> const& request)
{
m_parent->OnUpgradedConnection(request);
};
public:
/**
* @brief Constructs `%WinHttpTransport`.
*
* @param options Optional parameter to override the default settings.
*/
WinHttpTransportImpl(
WinHttpTransport const* parent,
WinHttpTransportOptions const& options = WinHttpTransportOptions());
/**
* @brief Constructs `%WinHttpTransport`.
*
* @param options Optional parameter to override the default settings.
*/
/**
* @brief Constructs `%WinHttpTransport` object based on common Azure HTTP Transport Options
*
*/
WinHttpTransportImpl(
WinHttpTransport const* parent,
Azure::Core::Http::Policies::TransportOptions const& options);
/**
* @brief Implements the HTTP transport interface to send an HTTP Request and produce an
* HTTP RawResponse.
*
*/
virtual std::unique_ptr<RawResponse> Send(Request& request, Context const& context) override;
// See also:
// [Core Guidelines C.35: "A base class destructor should be either public
// and virtual or protected and
// non-virtual"](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c35-a-base-class-destructor-should-be-either-public-and-virtual-or-protected-and-non-virtual)
virtual ~WinHttpTransportImpl();
};
}}}} // namespace Azure::Core::Http::_detail

View File

@ -28,9 +28,10 @@
#pragma warning(disable : 6553)
#pragma warning(disable : 6387) // An argument in result_macros.h may be '0', for the function
// 'GetProcAddress'.
#include <wincrypt.h>
#include <wil\resource.h>
#pragma warning(pop)
#include <wincrypt.h>
#include <winhttp.h>
namespace Azure { namespace Core { namespace Http { namespace _detail {
@ -157,6 +158,7 @@ namespace Azure { namespace Core { namespace Http { namespace _detail {
Azure::Core::_internal::UniqueHandle<HINTERNET> m_requestHandle;
std::unique_ptr<WinHttpAction> m_httpAction;
std::vector<std::string> m_expectedTlsRootCertificates;
wil::unique_cert_context m_tlsClientCertificate;
/*
* Adds the specified trusted certificates to the specified certificate store.
@ -176,6 +178,7 @@ namespace Azure { namespace Core { namespace Http { namespace _detail {
Azure::Core::_internal::UniqueHandle<HINTERNET> const& connectionHandle,
Azure::Core::Url const& url,
Azure::Core::Http::HttpMethod const& method,
PCCERT_CONTEXT tlsClientCertificate,
WinHttpTransportOptions const& options);
~WinHttpRequest();

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,17 @@ namespace Azure { namespace Core { namespace Test {
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, host);
auto response = m_pipeline->Send(request, Context{});
checkResponseCode(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::NoContent);
auto expectedResponseBodySize = std::stoull(response->GetHeaders().at("content-length"));
std::uint64_t expectedResponseBodySize;
if (response->GetStatusCode() == Azure::Core::Http::HttpStatusCode::NoContent)
{
// http://mt3.google.com/generate_204 returns 204 with no body and thus no content-length
// header
expectedResponseBodySize = 0;
}
else
{
expectedResponseBodySize = std::stoull(response->GetHeaders().at("content-length"));
}
CheckBodyFromBuffer(*response, expectedResponseBodySize);
}