Added functionality to enable CRL checking for CURL on linux; added tests for this new functionality. (#3923)

# Added functionality to enable CRL checking for CURL on linux.

This one is somewhat unpleasant and much larger than expected.

This pull request enables two pieces of functionality:
1. The ability to specify a known root certificate to the CURL HTTP transport (instead of a certificate file).
2. The ability to enable CRL validation (normally this is disabled in libCURL).

Enabling CRL validation ended up pulling in a significant chunk of code from azure-c-shared-util which handled retrieving CRLs (I was unable to find code in libCURL to do this). Native LibCURL support for CRL validation is limited to the schannel SSL backend (Windows Only).

This change also adds logic to the CURL transport to enable the ability to ignore CRL retrieval errors (there doesn't seem to be a comparable way of doing this for WinHTTP so it is a CURL transport only option).

To verify the root certificate logic, an extremely simple client for the SDK Test Proxy was written and is used to "record" a request to the C++ SDK HTTP server.
This commit is contained in:
Larry Osterman 2022-09-19 11:04:03 -07:00 committed by GitHub
parent bcf83a48cb
commit ceca1cf156
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1910 additions and 470 deletions

View File

@ -33,10 +33,10 @@ parameters:
type: object
default: []
- name: PreTestSteps
type: object
type: stepList
default: []
- name: PostTestSteps
type: object
type: stepList
default: []
jobs:

View File

@ -47,10 +47,10 @@ parameters:
type: boolean
default: false
- name: PreTestSteps
type: object
type: stepList
default: []
- name: PostTestSteps
type: object
type: stepList
default: []
@ -145,7 +145,7 @@ jobs:
Env: "$(CmakeEnvArg)"
- ${{ parameters.PreTestSteps }}
- pwsh: |
ctest `
-C Debug `
@ -156,7 +156,7 @@ jobs:
-T Test
workingDirectory: build
displayName: Test
- ${{ parameters.PostTestSteps }}
- task: PublishTestResults@2

View File

@ -37,10 +37,10 @@ parameters:
type: boolean
default: false
- name: PreTestSteps
type: object
type: stepList
default: []
- name: PostTestSteps
type: object
type: stepList
default: []
jobs:

View File

@ -68,10 +68,10 @@ parameters:
type: string
default: ''
- name: PreTestSteps
type: object
type: stepList
default: []
- name: PostTestSteps
type: object
type: stepList
default: []

View File

@ -30,10 +30,10 @@ parameters:
type: string
default: ''
- name: PreTestSteps
type: object
type: stepList
default: []
- name: PostTestSteps
type: object
type: stepList
default: []
stages:

View File

@ -38,10 +38,31 @@ namespace Azure { namespace Core { namespace Http {
*
* @remark Libcurl does revocation list check by default for SSL backends that supports this
* feature. However, the Azure SDK overrides libcurl's behavior and disables the revocation list
* check by default.
*
* check by default. This ensures that the LibCURL behavior matches the WinHTTP behavior.
*/
bool EnableCertificateRevocationListCheck = false;
/**
* @brief This option allows SSL connections to proceed even if there is an error retrieving the
* Certificate Revocation List.
*
* @remark Note that this only works when LibCURL is configured to use openssl as its TLS
* provider. That functionally limits this check to Linux only, and then only when openssl is
* configured (the default).
*/
bool AllowFailedCrlRetrieval = false;
/**
* @brief A set of PEM encoded X.509 certificates and CRLs describing the certificates used to
* validate the server.
*
* @remark The Azure SDK will not directly validate these certificates.
*
* @remark More about this option:
* https://curl.haxx.se/libcurl/c/CURLOPT_CAINFO_BLOB.html
*
*/
std::string PemEncodedExpectedRootCertificates;
};
/**
@ -86,7 +107,8 @@ namespace Azure { namespace Core { namespace Http {
*/
std::string ProxyPassword;
/**
* @brief The string for the certificate authenticator is sent to libcurl handle directly.
* @brief Path to a PEM encoded file containing the certificate authorities sent to libcurl
* handle directly.
*
* @remark The Azure SDK will not check if the path is valid or not.
*
@ -95,6 +117,7 @@ namespace Azure { namespace Core { namespace Http {
*
*/
std::string CAInfo;
/**
* @brief All HTTP requests will keep the connection channel open to the service.
*
@ -106,6 +129,7 @@ namespace Azure { namespace Core { namespace Http {
* handle. It is `true` by default.
*/
bool HttpKeepAlive = true;
/**
* @brief This option determines whether libcurl verifies the authenticity of the peer's
* certificate.

View File

@ -144,17 +144,35 @@ namespace Azure { namespace Core { namespace Http { namespace Policies {
*
* @remark The URL for the proxy server to use for this connection.
*/
Azure::Nullable<std::string> HttpProxy;
Azure::Nullable<std::string> HttpProxy{};
/**
* @brief The username to use when authenticating with the proxy server.
*/
std::string ProxyUserName;
std::string ProxyUserName{};
/**
* @brief The password to use when authenticating with the proxy server.
*/
std::string ProxyPassword;
std::string ProxyPassword{};
/**
* @brief Enable TLS Certificate validation against a certificate revocation list.
*
* @remark Note that by default CRL validation is *disabled*.
*/
bool EnableCertificateRevocationListCheck{false};
/**
* @brief Base64 encoded DER representation of an X.509 certificate expected in the certificate
* chain used in TLS connections.
*
* @remark Note that with the schannel and sectransp crypto backends, settting the
* expected root certificate disables access to the system certificate store.
* This means that the expected root certificate is the only certificate that will be trusted.
*/
std::string ExpectedTlsRootCertificate{};
#endif // defined(CURL_ADAPTER) || defined(WINHTTP_ADAPTER)
#endif // !defined(BUILD_TRANSPORT_CUSTOM_ADAPTER)

View File

@ -4,6 +4,7 @@
/**
* @file
* @brief #Azure::Core::Http::HttpTransport implementation via WinHTTP.
* cspell:words HCERTIFICATECHAIN PCCERT CCERT HCERTCHAINENGINE HCERTSTORE
*/
#pragma once
@ -22,11 +23,13 @@
#define NOMINMAX
#endif
#include <windows.h>
#endif
#include <memory>
#include <type_traits>
#include <vector>
#include <wincrypt.h>
#include <winhttp.h>
namespace Azure { namespace Core { namespace Http {
@ -49,6 +52,61 @@ namespace Azure { namespace Core { namespace Http {
};
using unique_HINTERNET = std::unique_ptr<void, HINTERNET_deleter>;
// unique_ptr class wrapping a PCCERT_CHAIN_CONTEXT
struct HCERTIFICATECHAIN_deleter
{
void operator()(PCCERT_CHAIN_CONTEXT handle)
{
// unique_ptr class wrapping an HINTERNET handle
{
CertFreeCertificateChain(handle);
}
}
};
using unique_CCERT_CHAIN_CONTEXT
= std::unique_ptr<const CERT_CHAIN_CONTEXT, HCERTIFICATECHAIN_deleter>;
// unique_ptr class wrapping an HCERTCHAINENGINE handle
struct HCERTCHAINENGINE_deleter
{
void operator()(HCERTCHAINENGINE handle) noexcept
{
if (handle != nullptr)
{
CertFreeCertificateChainEngine(handle);
}
}
};
using unique_HCERTCHAINENGINE = std::unique_ptr<void, HCERTCHAINENGINE_deleter>;
// unique_ptr class wrapping an HCERTSTORE handle
struct HCERTSTORE_deleter
{
public:
void operator()(HCERTSTORE handle) noexcept
{
if (handle != nullptr)
{
CertCloseStore(handle, 0);
}
}
};
using unique_HCERTSTORE = std::unique_ptr<void, HCERTSTORE_deleter>;
// unique_ptr class wrapping a PCCERT_CONTEXT
struct CERTCONTEXT_deleter
{
public:
void operator()(PCCERT_CONTEXT handle) noexcept
{
if (handle != nullptr)
{
CertFreeCertificateContext(handle);
}
}
};
using unique_PCCERT_CONTEXT = std::unique_ptr<CERT_CONTEXT const, CERTCONTEXT_deleter>;
class WinHttpStream final : public Azure::Core::IO::BodyStream {
private:
_detail::unique_HINTERNET m_requestHandle;
@ -122,6 +180,11 @@ namespace Azure { namespace Core { namespace Http {
*/
bool EnableSystemDefaultProxy{false};
/**
* @brief If True, enables checks for certificate revocation.
*/
bool EnableCertificateRevocationListCheck{false};
/**
* @brief Proxy information.
*
@ -142,6 +205,13 @@ namespace Azure { namespace Core { namespace Http {
* @brief Password for proxy authentication.
*/
std::string ProxyPassword;
/**
* @brief Array of Base64 encoded DER encoded X.509 certificate. These certificates should form
* a chain of certificates which will be used to validate the server certificate sent by the
* server.
*/
std::vector<std::string> ExpectedTlsRootCertificates;
};
/**
@ -155,6 +225,7 @@ namespace Azure { namespace Core { namespace Http {
// This should remain immutable and not be modified after calling the ctor, to avoid threading
// issues.
_detail::unique_HINTERNET m_sessionHandle;
bool m_requestHandleClosed{false};
_detail::unique_HINTERNET CreateSessionHandle();
_detail::unique_HINTERNET CreateConnectionHandle(
@ -183,6 +254,34 @@ namespace Azure { namespace Core { namespace Http {
_detail::unique_HINTERNET& requestHandle,
HttpMethod requestMethod);
/*
* Callback from WinHTTP called after the TLS certificates are received when the caller sets
* expected TLS root certificates.
*/
static void CALLBACK StatusCallback(
HINTERNET hInternet,
DWORD_PTR dwContext,
DWORD dwInternetStatus,
LPVOID lpvStatusInformation,
DWORD dwStatusInformationLength) noexcept;
/*
* Callback from WinHTTP called after the TLS certificates are received when the caller sets
* expected TLS root certificates.
*/
void OnHttpStatusOperation(HINTERNET hInternet, DWORD dwInternetStatus);
/*
* Adds the specified trusted certificates to the specified certificate store.
*/
bool AddCertificatesToStore(
std::vector<std::string> const& trustedCertificates,
_detail::unique_HCERTSTORE const& hCertStore);
/*
* Verifies that the certificate context is in the trustedCertificates set of certificates.
*/
bool VerifyCertificatesInChain(
std::vector<std::string> const& trustedCertificates,
_detail::unique_PCCERT_CONTEXT const& serverCertificate);
// Callback to allow a derived transport to extract the request handle. Used for WebSocket
// transports.
protected:

View File

@ -47,4 +47,10 @@
#define AZ_PLATFORM_WINDOWS
#elif defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))
#define AZ_PLATFORM_POSIX
#if defined(__APPLE__) || defined(__MACH__)
#define AZ_PLATFORM_MAC
#else
#define AZ_PLATFORM_LINUX
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@ -120,13 +120,6 @@ namespace Azure { namespace Core { namespace Http { namespace _detail {
// private constructor to keep this as singleton.
CurlConnectionPool() { curl_global_init(CURL_GLOBAL_ALL); }
static int CurlLoggingCallback(
CURL* handle,
curl_infotype type,
char* data,
size_t size,
void* userp);
// Makes possible to know the number of current connections in the connection pool for an
// index
size_t ConnectionsOnPool(std::string const& host) { return ConnectionPoolIndex[host].size(); }

View File

@ -10,7 +10,6 @@
#pragma once
#include "azure/core/http/http.hpp"
#include <chrono>
#include <string>
@ -26,6 +25,8 @@
#pragma warning(pop)
#endif
typedef struct x509_store_ctx_st X509_STORE_CTX;
namespace Azure { namespace Core { namespace Http {
namespace _detail {
@ -143,92 +144,88 @@ namespace Azure { namespace Core { namespace Http {
curl_socket_t m_curlSocket;
std::chrono::steady_clock::time_point m_lastUseTime;
std::string m_connectionKey;
// CRL validation is disabled by default to be consistent with WinHTTP behavior
bool m_enableCrlValidation{false};
// Allow the connection to proceed if retrieving the CRL failed.
bool m_allowFailedCrlRetrieval{true};
static int CurlLoggingCallback(
CURL* handle,
curl_infotype type,
char* data,
size_t size,
void* userp);
static int CurlSslCtxCallback(CURL* curl, void* sslctx, void* parm);
int SslCtxCallback(CURL* curl, void* sslctx);
int VerifyCertificateError(int ok, X509_STORE_CTX* storeContext);
public:
/**
* @brief Construct CURL HTTP connection.
*
* @param handle CURL handle.
* @param request Remote request
* @param options Connection options.
* @param hostDisplayName Display name for remote host, used for diagnostics.
*
* @param connectionPropertiesKey CURL connection properties key
*/
CurlConnection(_detail::unique_CURL&& handle, std::string connectionPropertiesKey)
: m_handle(std::move(handle)), m_connectionKey(std::move(connectionPropertiesKey))
CurlConnection(
Azure::Core::Http::Request& request,
Azure::Core::Http::CurlTransportOptions const& options,
std::string const& hostDisplayName,
std::string const& connectionPropertiesKey);
/**
* @brief Destructor.
* @details Cleans up CURL (invokes `curl_easy_cleanup()`).
*/
~CurlConnection() override {}
std::string const& GetConnectionKey() const override { return this->m_connectionKey; }
/**
* @brief Update last usage time for the connection.
*
*/
void UpdateLastUsageTime() override { this->m_lastUseTime = std::chrono::steady_clock::now(); }
/**
* @brief Checks whether this CURL connection is expired.
* @return `true` if this connection is considered expired; otherwise, `false`.
*/
bool IsExpired() override
{
// Get the socket that libcurl is using from handle. Will use this to wait while
// reading/writing
// into wire
#if defined(_MSC_VER)
#pragma warning(push)
// C26812: The enum type 'CURLcode' is un-scoped. Prefer 'enum class' over 'enum' (Enum.3)
#pragma warning(disable : 26812)
#endif
auto result = curl_easy_getinfo(m_handle.get(), CURLINFO_ACTIVESOCKET, &m_curlSocket);
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
if (result != CURLE_OK)
{
throw Http::TransportException(
"Broken connection. Couldn't get the active sockect for it."
+ std::string(curl_easy_strerror(result)));
}
}
auto connectionOnWaitingTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - this->m_lastUseTime);
return connectionOnWaitingTimeMs.count() >= _detail::DefaultConnectionExpiredMilliseconds;
}
/**
* @brief Destructor.
* @details Cleans up CURL (invokes `curl_easy_cleanup()`).
*/
~CurlConnection() override {}
/**
* @brief This function is used when working with streams to pull more data from the wire.
* Function will try to keep pulling data from socket until the buffer is all written or until
* there is no more data to get from the socket.
*
* @param context A context to control the request lifetime.
* @param buffer ptr to buffer where to copy bytes from socket.
* @param bufferSize size of the buffer and the requested bytes to be pulled from wire.
* @return return the numbers of bytes pulled from socket. It can be less than what it was
* requested.
*/
size_t ReadFromSocket(uint8_t* buffer, size_t bufferSize, Context const& context) override;
std::string const& GetConnectionKey() const override { return this->m_connectionKey; }
/**
* @brief This method will use libcurl socket to write all the bytes from buffer.
*
* @remarks Hardcoded timeout is used in case a socket stop responding.
*
* @param context A context to control the request lifetime.
* @param buffer ptr to the data to be sent to wire.
* @param bufferSize size of the buffer to send.
* @return CURL_OK when response is sent successfully.
*/
CURLcode SendBuffer(uint8_t const* buffer, size_t bufferSize, Context const& context) override;
/**
* @brief Update last usage time for the connection.
*
*/
void UpdateLastUsageTime() override
{
this->m_lastUseTime = std::chrono::steady_clock::now();
}
/**
* @brief Checks whether this CURL connection is expired.
* @return `true` if this connection is considered expired; otherwise, `false`.
*/
bool IsExpired() override
{
auto connectionOnWaitingTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - this->m_lastUseTime);
return connectionOnWaitingTimeMs.count() >= _detail::DefaultConnectionExpiredMilliseconds;
}
/**
* @brief This function is used when working with streams to pull more data from the wire.
* Function will try to keep pulling data from socket until the buffer is all written or until
* there is no more data to get from the socket.
*
* @param context A context to control the request lifetime.
* @param buffer ptr to buffer where to copy bytes from socket.
* @param bufferSize size of the buffer and the requested bytes to be pulled from wire.
* @return return the numbers of bytes pulled from socket. It can be less than what it was
* requested.
*/
size_t ReadFromSocket(uint8_t* buffer, size_t bufferSize, Context const& context) override;
/**
* @brief This method will use libcurl socket to write all the bytes from buffer.
*
* @remarks Hardcoded timeout is used in case a socket stop responding.
*
* @param context A context to control the request lifetime.
* @param buffer ptr to the data to be sent to wire.
* @param bufferSize size of the buffer to send.
* @return CURL_OK when response is sent successfully.
*/
CURLcode SendBuffer(uint8_t const* buffer, size_t bufferSize, Context const& context)
override;
void Shutdown() override;
};
void Shutdown() override;
};
}}} // namespace Azure::Core::Http

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
#include "azure/core/http/policies/policy.hpp"
#include "azure/core/platform.hpp"
#if defined(BUILD_CURL_HTTP_TRANSPORT_ADAPTER)
#include "azure/core/http/curl_transport.hpp"
@ -11,6 +12,9 @@
#include "azure/core/http/win_http_transport.hpp"
#endif
#include <sstream>
#include <string>
using Azure::Core::Context;
using namespace Azure::Core::IO;
using namespace Azure::Core::Http;
@ -21,12 +25,31 @@ namespace Azure { namespace Core { namespace Http { namespace Policies { namespa
namespace {
bool AnyTransportOptionsSpecified(TransportOptions const& transportOptions)
{
return !(
!transportOptions.HttpProxy.HasValue() && transportOptions.ProxyPassword.empty()
&& transportOptions.ProxyUserName.empty());
return (
transportOptions.HttpProxy.HasValue() || !transportOptions.ProxyPassword.empty()
|| !transportOptions.ProxyUserName.empty()
|| transportOptions.EnableCertificateRevocationListCheck
|| !transportOptions.ExpectedTlsRootCertificate.empty());
}
std::string PemEncodeFromBase64(std::string const& base64, std::string const& pemType)
{
std::stringstream rv;
rv << "-----BEGIN " << pemType << "-----" << std::endl;
std::string encodedValue(base64);
// Insert crlf characters every 80 characters into the base64 encoded key to make it
// prettier.
size_t insertPos = 80;
while (insertPos < encodedValue.length())
{
encodedValue.insert(insertPos, "\r\n");
insertPos += 82; /* 80 characters plus the \r\n we just inserted */
}
rv << encodedValue << std::endl << "-----END " << pemType << "-----" << std::endl;
return rv.str();
}
// std::once_flag createTransportOnce;
// std::shared_ptr<HttpTransport> defaultTransport;
} // namespace
std::shared_ptr<HttpTransport> GetTransportAdapter(TransportOptions const& transportOptions)
@ -56,6 +79,24 @@ namespace Azure { namespace Core { namespace Http { namespace Policies { namespa
}
httpOptions.ProxyUserName = transportOptions.ProxyUserName;
httpOptions.ProxyPassword = transportOptions.ProxyPassword;
// Note that WinHTTP accepts a set of root certificates, even though transportOptions only
// specifies a single one.
if (!transportOptions.ExpectedTlsRootCertificate.empty())
{
httpOptions.ExpectedTlsRootCertificates.push_back(
transportOptions.ExpectedTlsRootCertificate);
}
if (transportOptions.EnableCertificateRevocationListCheck)
{
httpOptions.EnableCertificateRevocationListCheck;
}
// If you specify an expected TLS root certificate, you also need to enable ignoring unknown
// CAs.
if (!transportOptions.ExpectedTlsRootCertificate.empty())
{
httpOptions.IgnoreUnknownCertificateAuthority;
}
return std::make_shared<Azure::Core::Http::WinHttpTransport>(httpOptions);
}
else
@ -83,6 +124,16 @@ namespace Azure { namespace Core { namespace Http { namespace Policies { namespa
{
curlOptions.ProxyPassword = transportOptions.ProxyPassword;
}
curlOptions.SslOptions.EnableCertificateRevocationListCheck
= transportOptions.EnableCertificateRevocationListCheck;
if (!transportOptions.ExpectedTlsRootCertificate.empty())
{
curlOptions.SslOptions.PemEncodedExpectedRootCertificates
= PemEncodeFromBase64(transportOptions.ExpectedTlsRootCertificate, "CERTIFICATE");
}
return std::make_shared<Azure::Core::Http::CurlTransport>(curlOptions);
}
return defaultTransport;

View File

@ -1,8 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
// cspell:words HCERTIFICATECHAIN PCCERT CCERT HCERTCHAINENGINE HCERTSTORE
#include "azure/core/http/http.hpp"
#include "azure/core/base64.hpp"
#include "azure/core/diagnostics/logger.hpp"
#include "azure/core/internal/diagnostics/log.hpp"
#include "azure/core/internal/strings.hpp"
#if defined(BUILD_TRANSPORT_WINHTTP_ADAPTER)
@ -11,11 +15,15 @@
#include <Windows.h>
#include <algorithm>
#include <sstream>
#include <string>
#include <wincrypt.h>
#include <winhttp.h>
using Azure::Core::Context;
using namespace Azure::Core::Http;
using namespace Azure::Core::Diagnostics;
using namespace Azure::Core::Diagnostics::_internal;
namespace {
@ -196,9 +204,217 @@ std::string GetHeadersAsString(Azure::Core::Http::Request const& request)
return requestHeaderString;
}
} // namespace
// For each certificate specified in trustedCertificate, add to certificateStore.
bool WinHttpTransport::AddCertificatesToStore(
std::vector<std::string> const& trustedCertificates,
_detail::unique_HCERTSTORE const& certificateStore)
{
for (auto const& trustedCertificate : trustedCertificates)
{
auto derCertificate = Azure::Core::Convert::Base64Decode(trustedCertificate);
if (!CertAddEncodedCertificateToStore(
certificateStore.get(),
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
derCertificate.data(),
static_cast<DWORD>(derCertificate.size()),
CERT_STORE_ADD_NEW,
NULL))
{
GetErrorAndThrow("CertAddEncodedCertificateToStore failed");
}
}
return true;
}
// VerifyCertificateInChain determines whether the certificate in serverCertificate
// chains up to the PEM represented by trustedCertificate or not.
bool WinHttpTransport::VerifyCertificatesInChain(
std::vector<std::string> const& trustedCertificates,
_detail::unique_PCCERT_CONTEXT const& serverCertificate)
{
if ((trustedCertificates.empty()) || !serverCertificate)
{
return false;
}
// Creates an in-memory certificate store that is destroyed at end of this function.
_detail::unique_HCERTSTORE certificateStore(CertOpenStore(
CERT_STORE_PROV_MEMORY,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_STORE_CREATE_NEW_FLAG,
nullptr));
if (!certificateStore)
{
GetErrorAndThrow("CertOpenStore failed");
}
// Add the trusted certificates to that store.
if (!AddCertificatesToStore(trustedCertificates, certificateStore))
{
Log::Write(Logger::Level::Error, "Cannot add certificates to store");
return false;
}
_detail::unique_HCERTCHAINENGINE certificateChainEngine;
{
CERT_CHAIN_ENGINE_CONFIG EngineConfig{};
EngineConfig.cbSize = sizeof(EngineConfig);
EngineConfig.dwFlags = CERT_CHAIN_ENABLE_CACHE_AUTO_UPDATE | CERT_CHAIN_ENABLE_SHARE_STORE;
EngineConfig.hExclusiveRoot = certificateStore.get();
HCERTCHAINENGINE engineHandle;
if (!CertCreateCertificateChainEngine(&EngineConfig, &engineHandle))
{
GetErrorAndThrow("CertCreateCertificateChainEngine failed");
}
certificateChainEngine.reset(engineHandle);
}
// Generate a certificate chain using the local chain engine and the certificate store containing
// the trusted certificates.
_detail::unique_CCERT_CHAIN_CONTEXT chainContextToVerify;
{
CERT_CHAIN_PARA ChainPara{};
ChainPara.cbSize = sizeof(ChainPara);
PCCERT_CHAIN_CONTEXT chainContext;
if (!CertGetCertificateChain(
certificateChainEngine.get(),
serverCertificate.get(),
nullptr,
certificateStore.get(),
&ChainPara,
0,
nullptr,
&chainContext))
{
GetErrorAndThrow("CertGetCertificateChain failed");
}
chainContextToVerify.reset(chainContext);
}
// And make sure that the certificate chain which was created matches the SSL chain.
{
CERT_CHAIN_POLICY_PARA PolicyPara{};
PolicyPara.cbSize = sizeof(PolicyPara);
CERT_CHAIN_POLICY_STATUS PolicyStatus{};
PolicyStatus.cbSize = sizeof(PolicyStatus);
if (!CertVerifyCertificateChainPolicy(
CERT_CHAIN_POLICY_SSL, chainContextToVerify.get(), &PolicyPara, &PolicyStatus))
{
GetErrorAndThrow("CertVerifyCertificateChainPolicy");
}
if (PolicyStatus.dwError != 0)
{
Log::Write(
Logger::Level::Error,
"CertVerifyCertificateChainPolicy sets certificateStatus "
+ std::to_string(PolicyStatus.dwError));
return false;
}
}
return true;
}
/**
* Called by WinHTTP when sending a request to the server. This callback allows us to inspect the
* TLS certificate before sending it to the server.
*/
void WinHttpTransport::StatusCallback(
HINTERNET hInternet,
DWORD_PTR dwContext,
DWORD dwInternetStatus,
LPVOID,
DWORD) noexcept
{
// If we're called before our context has been set (on Open and Close callbacks), ignore the
// status callback.
if (dwContext == 0)
{
return;
}
try
{
WinHttpTransport* httpTransport = reinterpret_cast<WinHttpTransport*>(dwContext);
httpTransport->OnHttpStatusOperation(hInternet, dwInternetStatus);
}
catch (Azure::Core::RequestFailedException& rfe)
{
// If an exception is thrown in the handler, log the error and terminate the connection.
Log::Write(
Logger::Level::Error,
"Request Failed Exception Thrown: " + std::string(rfe.what()) + rfe.Message);
WinHttpCloseHandle(hInternet);
}
catch (std::exception& ex)
{
// If an exception is thrown in the handler, log the error and terminate the connection.
Log::Write(Logger::Level::Error, "Exception Thrown: " + std::string(ex.what()));
}
}
/**
* @brief HTTP Callback to enable private certificate checks.
*
* This method is called by WinHTTP when a certificate is received. This method is called multiple
* times based on the state of the TLS connection. We are only interested in
* WINHTTP_CALLBACK_STATUS_SENDING_REQUEST, which is called during the TLS handshake.
*
* When called, we verify that the certificate chain sent from the server contains the certificate
* the HTTP client was configured with. If it is, we accept the connection, if it is not,
* we abort the connection, closing the incoming request handle.
*/
void WinHttpTransport::OnHttpStatusOperation(HINTERNET hInternet, DWORD dwInternetStatus)
{
if (dwInternetStatus != WINHTTP_CALLBACK_STATUS_SENDING_REQUEST)
{
if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_SECURE_FAILURE)
{
Log::Write(Logger::Level::Error, "Security failure. :(");
}
// Silently ignore if there's any statuses we get that we can't handle
return;
}
// We will only set the Status callback if a root certificate has been set.
AZURE_ASSERT(!m_options.ExpectedTlsRootCertificates.empty());
// Ask WinHTTP for the server certificate - this won't be valid outside a status callback.
_detail::unique_PCCERT_CONTEXT serverCertificate;
{
PCCERT_CONTEXT certContext;
DWORD bufferLength = sizeof(certContext);
if (!WinHttpQueryOption(
hInternet,
WINHTTP_OPTION_SERVER_CERT_CONTEXT,
reinterpret_cast<void*>(&certContext),
&bufferLength))
{
GetErrorAndThrow("Could not retrieve TLS server certificate.");
}
serverCertificate.reset(certContext);
}
if (!VerifyCertificatesInChain(m_options.ExpectedTlsRootCertificates, serverCertificate))
{
Log::Write(Logger::Level::Error, "Server certificate is not trusted. Aborting HTTP request");
// To signal to caller that the request is to be terminated, the callback closes the handle.
// This ensures that no message is sent to the server.
WinHttpCloseHandle(hInternet);
// To avoid a double free of this handle record that we've
// already closed the handle.
m_requestHandleClosed = true;
}
}
void WinHttpTransport::GetErrorAndThrow(const std::string& exceptionMessage, DWORD error)
{
std::string errorMessage = exceptionMessage + " Error Code: " + std::to_string(error);
@ -277,6 +493,21 @@ _detail::unique_HINTERNET WinHttpTransport::CreateSessionHandle()
GetErrorAndThrow("Error while enforcing TLS 1.2 for connection request.");
}
if (!m_options.ExpectedTlsRootCertificates.empty())
{
// Set the callback function to be called when a server certificate is received.
if (WinHttpSetStatusCallback(
sessionHandle.get(),
&WinHttpTransport::StatusCallback,
WINHTTP_CALLBACK_FLAG_SEND_REQUEST /* WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS*/,
0)
== WINHTTP_INVALID_STATUS_CALLBACK)
{
GetErrorAndThrow("Error while setting up the status callback.");
}
}
return sessionHandle;
}
@ -396,7 +627,7 @@ _detail::unique_HINTERNET WinHttpTransport::CreateRequestHandle(
}
}
if (m_options.IgnoreUnknownCertificateAuthority)
if (m_options.IgnoreUnknownCertificateAuthority || !m_options.ExpectedTlsRootCertificates.empty())
{
auto option = SECURITY_FLAG_IGNORE_UNKNOWN_CA;
if (!WinHttpSetOption(request.get(), WINHTTP_OPTION_SECURITY_FLAGS, &option, sizeof(option)))
@ -405,6 +636,15 @@ _detail::unique_HINTERNET WinHttpTransport::CreateRequestHandle(
}
}
if (m_options.EnableCertificateRevocationListCheck)
{
DWORD value = WINHTTP_ENABLE_SSL_REVOCATION;
if (!WinHttpSetOption(request.get(), WINHTTP_OPTION_ENABLE_FEATURE, &value, sizeof(value)))
{
GetErrorAndThrow("Error while enabling CRL validation.");
}
}
// If we are supporting WebSockets, then let WinHTTP know that it should
// prepare to upgrade the HttpRequest to a WebSocket.
#pragma warning(push)
@ -491,7 +731,7 @@ void WinHttpTransport::SendRequest(
WINHTTP_NO_REQUEST_DATA,
0,
streamLength > 0 ? static_cast<DWORD>(streamLength) : 0,
0))
reinterpret_cast<DWORD_PTR>(this)))
{
// Errors include:
// ERROR_WINHTTP_CANNOT_CONNECT
@ -729,8 +969,22 @@ std::unique_ptr<RawResponse> WinHttpTransport::Send(Request& request, Context co
_detail::unique_HINTERNET connectionHandle = CreateConnectionHandle(request.GetUrl(), context);
_detail::unique_HINTERNET requestHandle
= CreateRequestHandle(connectionHandle, request.GetUrl(), request.GetMethod());
try
{
SendRequest(requestHandle, request, context);
}
catch (TransportException&)
{
// If there was a TLS validation error, then we will have closed the request handle
// during the TLS validation callback. So if an exception was thrown, if we force closed the
// request handle, clear the handle in the requestHandle to prevent a double free.
if (m_requestHandleClosed)
{
requestHandle.release();
}
SendRequest(requestHandle, request, context);
throw;
}
ReceiveResponse(requestHandle, context);

View File

@ -33,6 +33,11 @@ endif()
include(GoogleTest)
if (NOT WIN32)
find_package(OpenSSL REQUIRED)
SET(OPENSSLCRYPTO OpenSSL::Crypto)
endif()
add_executable (
azure-core-test
azure_core_test.cpp
@ -119,7 +124,7 @@ add_custom_command(TARGET azure-core-test POST_BUILD
# Adding private headers from CORE to the tests so we can test the private APIs with no relative paths include.
target_include_directories (azure-core-test PRIVATE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../src>)
target_link_libraries(azure-core-test PRIVATE azure-core gtest gmock)
target_link_libraries(azure-core-test PRIVATE azure-core gtest gmock ${OPENSSLCRYPTO})
create_map_file(azure-core-test azure-core-test.map)

View File

@ -55,7 +55,7 @@ namespace Azure { namespace Core { namespace Test {
Azure::Core::Http::Request req(
Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get()));
std::string const expectedConnectionKey(CreateConnectionKey(
AzureSdkHttpbinServer::Schema(), AzureSdkHttpbinServer::Host(), ",0,0,0,0,1,1,0,0"));
AzureSdkHttpbinServer::Schema(), AzureSdkHttpbinServer::Host(), ",0,0,0,0,1,1,0,0,0,0"));
{
// Creating a new connection with default options
@ -124,7 +124,7 @@ namespace Azure { namespace Core { namespace Test {
// Now test that using a different connection config won't re-use the same connection
std::string const secondExpectedKey = AzureSdkHttpbinServer::Schema() + "://"
+ AzureSdkHttpbinServer::Host() + ",0,0,0,0,1,0,0,200000";
+ AzureSdkHttpbinServer::Host() + ",0,0,0,0,1,0,0,0,0,200000";
{
// Creating a new connection with options
Azure::Core::Http::CurlTransportOptions options;
@ -433,7 +433,9 @@ namespace Azure { namespace Core { namespace Test {
Azure::Core::Http::Request req(
Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority));
std::string const expectedConnectionKey(CreateConnectionKey(
AzureSdkHttpbinServer::Schema(), AzureSdkHttpbinServer::Host(), ",0,0,0,0,1,1,0,0"));
AzureSdkHttpbinServer::Schema(),
AzureSdkHttpbinServer::Host(),
",0,0,0,0,1,1,0,0,0,0"));
// Creating a new connection with default options
auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
@ -471,7 +473,7 @@ namespace Azure { namespace Core { namespace Test {
std::string const expectedConnectionKey(CreateConnectionKey(
AzureSdkHttpbinServer::Schema(),
AzureSdkHttpbinServer::Host(),
":443,0,0,0,0,1,1,0,0"));
":443,0,0,0,0,1,1,0,0,0,0"));
// Creating a new connection with default options
auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
@ -508,7 +510,9 @@ namespace Azure { namespace Core { namespace Test {
Azure::Core::Http::Request req(
Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority));
std::string const expectedConnectionKey(CreateConnectionKey(
AzureSdkHttpbinServer::Schema(), AzureSdkHttpbinServer::Host(), ",0,0,0,0,1,1,0,0"));
AzureSdkHttpbinServer::Schema(),
AzureSdkHttpbinServer::Host(),
",0,0,0,0,1,1,0,0,0,0"));
// Creating a new connection with default options
auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool
@ -545,7 +549,7 @@ namespace Azure { namespace Core { namespace Test {
std::string const expectedConnectionKey(CreateConnectionKey(
AzureSdkHttpbinServer::Schema(),
AzureSdkHttpbinServer::Host(),
":443,0,0,0,0,1,1,0,0"));
":443,0,0,0,0,1,1,0,0,0,0"));
// Creating a new connection with default options
auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool

View File

@ -78,13 +78,16 @@ namespace Azure { namespace Core { namespace Test {
std::unique_ptr<Azure::Core::Http::RawResponse> response;
EXPECT_NO_THROW(response = pipeline.Send(request, Azure::Core::Context::ApplicationContext));
auto responseCode = response->GetStatusCode();
int expectedCode = 200;
EXPECT_PRED2(
[](int expected, int code) { return expected == code; },
expectedCode,
static_cast<typename std::underlying_type<Azure::Core::Http::HttpStatusCode>::type>(
responseCode));
if (response)
{
auto responseCode = response->GetStatusCode();
int expectedCode = 200;
EXPECT_PRED2(
[](int expected, int code) { return expected == code; },
expectedCode,
static_cast<typename std::underlying_type<Azure::Core::Http::HttpStatusCode>::type>(
responseCode));
}
// Clean the connection from the pool *Windows fails to clean if we leave to be clean upon
// app-destruction

View File

@ -4,6 +4,7 @@
#include "azure/core/context.hpp"
#include "azure/core/http/curl_transport.hpp"
#include "azure/core/http/policies/policy.hpp"
#include "azure/core/internal/client_options.hpp"
#include "azure/core/internal/environment.hpp"
#include "azure/core/internal/http/pipeline.hpp"
#include "azure/core/internal/json/json.hpp"
@ -14,7 +15,8 @@
#include <string>
#include <thread>
#if !defined(DISABLE_PROXY_TESTS)
using namespace std::chrono_literals;
namespace Azure { namespace Core { namespace Test {
namespace {
constexpr static const char AzureSdkHttpbinServerSchema[] = "https";
@ -90,33 +92,8 @@ namespace Azure { namespace Core { namespace Test {
Azure::Core::Http::HttpStatusCode code,
Azure::Core::Http::HttpStatusCode expectedCode = Azure::Core::Http::HttpStatusCode::Ok);
std::string HttpProxyServer()
{
std::string anonymousServer{
Azure::Core::_internal::Environment::GetVariable("ANONYMOUSCONTAINERIPV4ADDRESS")};
GTEST_LOG_(INFO) << "Anonymous server: " << anonymousServer;
if (anonymousServer.empty())
{
GTEST_LOG_(ERROR)
<< "Could not find value for ANONYMOUSCONTAINERIPV4ADDRESS, Assuming local.";
anonymousServer = "127.0.0.1";
}
return "http://" + anonymousServer + ":3128";
}
std::string HttpProxyServerWithPassword()
{
std::string authenticatedServer{
Azure::Core::_internal::Environment::GetVariable("AUTHENTICATEDCONTAINERIPV4ADDRESS")};
GTEST_LOG_(INFO) << "Authenticated server: " << authenticatedServer;
if (authenticatedServer.empty())
{
GTEST_LOG_(ERROR)
<< "Could not find value for AUTHENTICATEDCONTAINERIPV4ADDRESS, Assuming local.";
authenticatedServer = "127.0.0.1";
}
return "http://" + authenticatedServer + ":3129";
}
std::string HttpProxyServer() { return "http://127.0.0.1:3128"; }
std::string HttpProxyServerWithPassword() { return "http://127.0.0.1:3129"; }
protected:
// Create
@ -247,6 +224,7 @@ namespace Azure { namespace Core { namespace Test {
using namespace Azure::Core::Http::_internal;
using namespace Azure::Core::Http::Policies::_internal;
#if !defined(DISABLE_PROXY_TESTS)
// constexpr char SocksProxyServer[] = "socks://98.162.96.41:4145";
TEST_F(TransportAdapterOptions, SimpleProxyTests)
{
@ -377,5 +355,484 @@ namespace Azure { namespace Core { namespace Test {
CheckBodyFromBuffer(*response, expectedResponseBodySize);
}
}
}}} // namespace Azure::Core::Test
#endif // defined(DISABLE_PROXY_TESTS)
TEST_F(TransportAdapterOptions, DisableCrlValidation)
{
Azure::Core::Url testUrl(AzureSdkHttpbinServer::Get());
// Azure::Core::Url testUrl("https://www.microsoft.com/");
// HTTP Connections.
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
transportOptions.EnableCertificateRevocationListCheck = true;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, testUrl);
auto response = pipeline.Send(request, Azure::Core::Context::ApplicationContext);
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
#if !defined(DISABLE_PROXY_TESTS)
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
transportOptions.HttpProxy = HttpProxyServerWithPassword();
transportOptions.ProxyUserName = "user";
transportOptions.ProxyPassword = "password";
// Disable CA checks on proxy pipelines too.
transportOptions.EnableCertificateRevocationListCheck = true;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, testUrl);
auto response = pipeline.Send(request, Azure::Core::Context::ApplicationContext);
checkResponseCode(response->GetStatusCode());
auto expectedResponseBodySize = std::stoull(response->GetHeaders().at("content-length"));
CheckBodyFromBuffer(*response, expectedResponseBodySize);
}
#endif
}
TEST_F(TransportAdapterOptions, CheckFailedCrlValidation)
{
// By default, for the Windows and Mac platforms, Curl uses
// SCHANNEL/SECTRANSP for CRL validation. Those SSL protocols
// don't have the same behaviors as OpenSSL does.
#if !defined(AZ_PLATFORM_WINDOWS) && !defined(AZ_PLATFORM_MAC)
// Azure::Core::Url
// testUrl("https://github.com/Azure/azure-sdk-for-cpp/blob/main/README.md");
Azure::Core::Url testUrl("https://www.wikipedia.org");
// For <reasons>, github URLs work just fine if CRL validation is off, but if enabled,
// they fail. Let's use that fact to verify that CRL validation causes github
// URLs to fail.
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
transportOptions.EnableCertificateRevocationListCheck = false;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
{
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, testUrl);
auto response = pipeline.Send(request, Azure::Core::Context::ApplicationContext);
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
}
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
transportOptions.EnableCertificateRevocationListCheck = true;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
{
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, testUrl);
EXPECT_THROW(
pipeline.Send(request, Azure::Core::Context::ApplicationContext),
Azure::Core::Http::TransportException);
}
}
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
//
// Retrieving the test URL should succeed if we allow failed CRL retrieval because
// the certificate for the test URL doesn't contain a CRL distribution points extension,
// and by default there is no platform CRL present.
Azure::Core::Http::CurlTransportOptions curlOptions;
curlOptions.SslOptions.AllowFailedCrlRetrieval = true;
curlOptions.SslOptions.EnableCertificateRevocationListCheck = true;
transportOptions.Transport = std::make_shared<Azure::Core::Http::CurlTransport>(curlOptions);
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
{
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, testUrl);
auto response = pipeline.Send(request, Azure::Core::Context::ApplicationContext);
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
}
#endif
}
TEST_F(TransportAdapterOptions, MultipleCrlOperations)
{
std::vector<std::string> testUrls{
AzureSdkHttpbinServer::Get(),
"https://www.microsoft.com/",
"https://www.example.com/",
"https://www.google.com/",
};
GTEST_LOG_(INFO) << "Basic test calls.";
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// FIrst verify connectivity to the test servers.
transportOptions.EnableCertificateRevocationListCheck = false;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
for (auto const& target : testUrls)
{
GTEST_LOG_(INFO) << "Test " << target;
Azure::Core::Url url(target);
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, url);
std::unique_ptr<Azure::Core::Http::RawResponse> response;
EXPECT_NO_THROW(
response = pipeline.Send(request, Azure::Core::Context::ApplicationContext));
if (response && response->GetStatusCode() != Azure::Core::Http::HttpStatusCode::Found)
{
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
}
}
// Now verify that once we enable CRL checks, we can still access the URLs.
GTEST_LOG_(INFO) << "Test with CRL checks enabled";
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
transportOptions.EnableCertificateRevocationListCheck = true;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
for (auto const& target : testUrls)
{
GTEST_LOG_(INFO) << "Test " << target;
Azure::Core::Url url(target);
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, url);
std::unique_ptr<Azure::Core::Http::RawResponse> response;
EXPECT_NO_THROW(
response = pipeline.Send(request, Azure::Core::Context::ApplicationContext));
if (response && response->GetStatusCode() != Azure::Core::Http::HttpStatusCode::Found)
{
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
}
}
// Now verify that once we enable CRL checks, we can still access the URLs.
GTEST_LOG_(INFO) << "Test with CRL checks enabled. Iteration 2.";
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
transportOptions.EnableCertificateRevocationListCheck = true;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
for (auto const& target : testUrls)
{
GTEST_LOG_(INFO) << "Test " << target;
Azure::Core::Url url(target);
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, url);
std::unique_ptr<Azure::Core::Http::RawResponse> response;
EXPECT_NO_THROW(
response = pipeline.Send(request, Azure::Core::Context::ApplicationContext));
if (response && response->GetStatusCode() != Azure::Core::Http::HttpStatusCode::Found)
{
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
}
}
}
TEST_F(TransportAdapterOptions, TestRootCertificate)
{
// On Windows and OSX, setting a root certificate disables the default system certificate
// store. That means that if we set the expected certificate, we won't be able to connect to
// the server because the certificates root CA is not in the store.
#if defined(AZ_PLATFORM_LINUX)
// cspell:disable
std::string azurewebsitesCertificate
= "MIIF8zCCBNugAwIBAgIQCq+mxcpjxFFB6jvh98dTFzANBgkqhkiG9w0BAQwFADBh"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH"
"MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT"
"MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv"
"c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwMTCCAiIwDQYJKoZIhvcNAQEBBQAD"
"ggIPADCCAgoCggIBAMedcDrkXufP7pxVm1FHLDNA9IjwHaMoaY8arqqZ4Gff4xyr"
"RygnavXL7g12MPAx8Q6Dd9hfBzrfWxkF0Br2wIvlvkzW01naNVSkHp+OS3hL3W6n"
"l/jYvZnVeJXjtsKYcXIf/6WtspcF5awlQ9LZJcjwaH7KoZuK+THpXCMtzD8XNVdm"
"GW/JI0C/7U/E7evXn9XDio8SYkGSM63aLO5BtLCv092+1d4GGBSQYolRq+7Pd1kR"
"EkWBPm0ywZ2Vb8GIS5DLrjelEkBnKCyy3B0yQud9dpVsiUeE7F5sY8Me96WVxQcb"
"OyYdEY/j/9UpDlOG+vA+YgOvBhkKEjiqygVpP8EZoMMijephzg43b5Qi9r5UrvYo"
"o19oR/8pf4HJNDPF0/FJwFVMW8PmCBLGstin3NE1+NeWTkGt0TzpHjgKyfaDP2tO"
"4bCk1G7pP2kDFT7SYfc8xbgCkFQ2UCEXsaH/f5YmpLn4YPiNFCeeIida7xnfTvc4"
"7IxyVccHHq1FzGygOqemrxEETKh8hvDR6eBdrBwmCHVgZrnAqnn93JtGyPLi6+cj"
"WGVGtMZHwzVvX1HvSFG771sskcEjJxiQNQDQRWHEh3NxvNb7kFlAXnVdRkkvhjpR"
"GchFhTAzqmwltdWhWDEyCMKC2x/mSZvZtlZGY+g37Y72qHzidwtyW7rBetZJAgMB"
"AAGjggGtMIIBqTAdBgNVHQ4EFgQUDyBd16FXlduSzyvQx8J3BM5ygHYwHwYDVR0j"
"BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud"
"JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG"
"CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu"
"Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln"
"aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j"
"cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG"
"MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j"
"cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD"
"AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQAlFvNh7QgXVLAZSsNR2XRmIn9iS8OHFCBA"
"WxKJoi8YYQafpMTkMqeuzoL3HWb1pYEipsDkhiMnrpfeYZEA7Lz7yqEEtfgHcEBs"
"K9KcStQGGZRfmWU07hPXHnFz+5gTXqzCE2PBMlRgVUYJiA25mJPXfB00gDvGhtYa"
"+mENwM9Bq1B9YYLyLjRtUz8cyGsdyTIG/bBM/Q9jcV8JGqMU/UjAdh1pFyTnnHEl"
"Y59Npi7F87ZqYYJEHJM2LGD+le8VsHjgeWX2CJQko7klXvcizuZvUEDTjHaQcs2J"
"+kPgfyMIOY1DMJ21NxOJ2xPRC/wAh/hzSBRVtoAnyuxtkZ4VjIOh";
// cspell:enable
{
Azure::Core::Http::Policies::TransportOptions transportOptions;
// Note that the default is to *disable* CRL checks, because they are disabled
// by default. So we test *enabling* CRL validation checks.
transportOptions.ExpectedTlsRootCertificate = azurewebsitesCertificate;
HttpPipeline pipeline(CreateHttpPipeline(transportOptions));
Azure::Core::Url url(AzureSdkHttpbinServer::Get());
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, url);
auto response = pipeline.Send(request, Azure::Core::Context::ApplicationContext);
EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::Ok);
}
#endif
}
const std::string TestProxyHttpsCertificate =
// cspell:disable
"MIIDSDCCAjCgAwIBAgIUIoKu8Oao7j10TLNxaUG2Bs0FrRwwDQYJKoZIhvcNAQEL"
"BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDgwNTIxMTcyM1oXDTIzMDgw"
"NTIxMTcyM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF"
"AAOCAQ8AMIIBCgKCAQEA0UPG7ER++5/9D/qa4SCtt7QvdHwcpidbwktPNU8iRW7V"
"pIDPWS4goLp/+7+maT0Z/mqwSO3JDtm/dtdlr3F/5EMgyUExnYcvUixZAiyFyEwj"
"j6wnAtNvqsg4rDqBlD17fuqTVsZm9Yo7QYub6p5PeznWYucOxRrczqFCiW4uj0Yk"
"GgUHPPmCvhSDKowV8CYRHfkD6R8R4SFkoP3/uejXHxeXoYJNMWq5K0GqGaOZtNFB"
"F7QWZHoLrRpZcY4h+DxwP3c+/FdlVcs9nstkF+EnTnwx5IRyKsaWb/pUEmYKvNDz"
"wi6qnRUdu+DghZuvyZZDgwoYrSZokcbKumk0MsLC3QIDAQABo4GRMIGOMA8GA1Ud"
"EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGmMBYGA1UdJQEB/wQMMAoGCCsGAQUF"
"BwMBMBcGA1UdEQEB/wQNMAuCCWxvY2FsaG9zdDA6BgorBgEEAYI3VAEBBCwMKkFT"
"UC5ORVQgQ29yZSBIVFRQUyBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTANBgkqhkiG"
"9w0BAQsFAAOCAQEARX4NxGbycdPVuqvu/CO+/LpWrEm1OcOl7N57/mD5npTIJT78"
"TYtXk1J61akumKdf5CaBgCDRcl35LhioFZIMEsiOidffAp6t493xocncFBhIYYrZ"
"HS6aKsZKPu8h3wOLpYu+zh7f0Hx6pkHPAfw4+knmQjDYomz/hTwuo/MuT8k6Ee7B"
"NGWqxUamLI8bucuf2ZfT1XOq83uWaFF5KwAuVLhpzo39/TmPyYGnaoKRYf9QjabS"
"LUjecMNLJFWHUSD4cKHvXJjDYZEiCiy+MdUDytWIsfw0fzAUjz9Qaz8YpZ+fXufM"
"MNMNfyJHSMEMFIT2D1UaQiwryXWQWJ93OiSdjA==";
const std::string InvalidTestProxyHttpsCertificate
= "MIIIujCCBqKgAwIBAgITMwAxS6DhmVCLBf6MWwAAADFLoDANBgkqhkiG9w0BAQwF"
"ADBZMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u"
"MSowKAYDVQQDEyFNaWNyb3NvZnQgQXp1cmUgVExTIElzc3VpbmcgQ0EgMDEwHhcN"
"MjIwMzE0MTgzOTU1WhcNMjMwMzA5MTgzOTU1WjBqMQswCQYDVQQGEwJVUzELMAkG"
"A1UECBMCV0ExEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD"
"b3Jwb3JhdGlvbjEcMBoGA1UEAwwTKi5henVyZXdlYnNpdGVzLm5ldDCCASIwDQYJ"
"KoZIhvcNAQEBBQADggEPADCCAQoCggEBAM3heDMqn7v8cmh4A9vECuEfuiUnKBIw"
"7y0Sf499Z7WW92HDkIvV3eJ6jcyq41f2UJcG8ivCu30eMnYyyI+aRHIedkvOBA2i"
"PqG78e99qGTuKCj9lrJGVfeTBJ1VIlPvfuHFv/3JaKIBpRtuqxCdlgsGAJQmvHEn"
"vIHUV2jgj4iWNBDoC83ShtWg6qV2ol7yiaClB20Af5byo36jVdMN6vS+/othn3jG"
"pn+NP00DWYbP5y4qhs5XLH9wQZaTUPKIaUxmHewErcM0rMAaWl8wMqQTeNYf3l5D"
"ax50yuEg9VVjtbDdSmvOkslGpVqsOl1NrmyN7gCvcvcRUQcxIiXJQc0CAwEAAaOC"
"BGgwggRkMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdgCt9776fP8QyIudPZwe"
"PhhqtGcpXc+xDCTKhYY069yCigAAAX+Jw/reAAAEAwBHMEUCIE8AAjvwO4AffPn7"
"un67WykJ2hGB4n8qJE7pk4QYjWW+AiEA/pio1E9ALt30Kh/Ga4gRefH1ILbQ8n4h"
"bHFatezIcvYAdwB6MoxU2LcttiDqOOBSHumEFnAyE4VNO9IrwTpXo1LrUgAAAX+J"
"w/qlAAAEAwBIMEYCIQCdbj6FOX6wK+dLoqjWKuCgkKSsZsJKpVik6HjlRgomzQIh"
"AM7mYp5dBFmNLas3fFcP0rMMK+17n8u0GhFH2KpkPr1SAHYA6D7Q2j71BjUy51co"
"vIlryQPTy9ERa+zraeF3fW0GvW4AAAF/icP6jgAABAMARzBFAiAhjTz3PBjqRrpY"
"eH7us44lESC7c0dzdTcehTeAwmEyrgIhAOCaqmqA+ercv+39jzFWkctG36bazRFX"
"4gGNiKU0bctcMCcGCSsGAQQBgjcVCgQaMBgwCgYIKwYBBQUHAwIwCgYIKwYBBQUH"
"AwEwPAYJKwYBBAGCNxUHBC8wLQYlKwYBBAGCNxUIh73XG4Hn60aCgZ0ujtAMh/Da"
"HV2ChOVpgvOnPgIBZAIBJTCBrgYIKwYBBQUHAQEEgaEwgZ4wbQYIKwYBBQUHMAKG"
"YWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0"
"JTIwQXp1cmUlMjBUTFMlMjBJc3N1aW5nJTIwQ0ElMjAwMSUyMC0lMjB4c2lnbi5j"
"cnQwLQYIKwYBBQUHMAGGIWh0dHA6Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2Nz"
"cDAdBgNVHQ4EFgQUiiks5RXI6IIQccflfDtgAHndN7owDgYDVR0PAQH/BAQDAgSw"
"MHwGA1UdEQR1MHOCEyouYXp1cmV3ZWJzaXRlcy5uZXSCFyouc2NtLmF6dXJld2Vi"
"c2l0ZXMubmV0ghIqLmF6dXJlLW1vYmlsZS5uZXSCFiouc2NtLmF6dXJlLW1vYmls"
"ZS5uZXSCFyouc3NvLmF6dXJld2Vic2l0ZXMubmV0MAwGA1UdEwEB/wQCMAAwZAYD"
"VR0fBF0wWzBZoFegVYZTaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j"
"cmwvTWljcm9zb2Z0JTIwQXp1cmUlMjBUTFMlMjBJc3N1aW5nJTIwQ0ElMjAwMS5j"
"cmwwZgYDVR0gBF8wXTBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0"
"cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRt"
"MAgGBmeBDAECAjAfBgNVHSMEGDAWgBQPIF3XoVeV25LPK9DHwncEznKAdjAdBgNV"
"HSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEMBQADggIBAKtk"
"4nEDfqxbP80uaoBoPaeeX4G/tBNcfpR2sf6soW8atAqGOohdLPcE0n5/KJn+H4u7"
"CsZdTJyUVxBxAlpqAc9JABl4urWNbhv4pueGBZXOn5K5Lpup/gp1HhCx4XKFno/7"
"T22NVDol4LRLUTeTkrpNyYLU5QYBQpqlFMAcvem/2seiPPYghFtLr5VWVEikUvnf"
"wSlECNk84PT7mOdbrX7T3CbG9WEZVmSYxMCS4pwcW3caXoSzUzZ0H1sJndCJW8La"
"9tekRKkMVkN558S+FFwaY1yARNqCFeK+yiwvkkkojqHbgwFJgCFWYy37kFR9uPiv"
"3sTHvs8IZ5K8TY7rHk3pSMYqoBTODCs7wKGiByWSDMcfAgGBzjt95SKfq0p6sj0C"
"+HWFiyKR+PTi2esFP9Vr9sC9jfRM6zwa7KnONqLefHauJPdNMt5l1FQGWvyco4IN"
"lwK3Z9FfEOFZA4YcjsqnkNacKZqLjgis3FvD8VPXETgRuffVc75lJxH6WmkwqdXj"
"BlU8wOcJyXTmM1ehYpziCpWvGBSEIsFuK6BC/iBnQEuWKdctAdbHIDlLctGgDWjx"
"xYDPZ/TtORGL8YaDnj6QHeOURIAHCtt6NCWKV6OR2HtMx+tCEvfi5ION1dyJ9hAX"
"+4K9FXc71ab7tdV/GLPkWc8Q0x1nk7ogDYcqKbiF";
class TestProxy {
// cspell:enable
std::unique_ptr<HttpPipeline> m_pipeline;
public:
struct TestProxyOptions : Azure::Core::_internal::ClientOptions
{
TestProxyOptions() : Azure::Core::_internal::ClientOptions() {}
};
TestProxy(TestProxyOptions options = TestProxyOptions())
{
if (options.Transport.ExpectedTlsRootCertificate.empty())
{
options.Transport.ExpectedTlsRootCertificate = TestProxyHttpsCertificate;
}
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perRetryPolicies;
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perCallPolicies;
m_pipeline = std::make_unique<Azure::Core::Http::_internal::HttpPipeline>(
options,
"Test Proxy",
"2021-11",
std::move(perRetryPolicies),
std::move(perCallPolicies));
}
Azure::Response<std::string> PostStartRecording(std::string const& recordingFile)
{
std::string proxyServerRequest;
proxyServerRequest = "{ \"x-recording-file\": \"";
proxyServerRequest += Azure::Core::Url::Encode(recordingFile);
proxyServerRequest += "\"}";
std::vector<uint8_t> bodyVector{proxyServerRequest.begin(), proxyServerRequest.end()};
Azure::Core::IO::MemoryBodyStream postBody(bodyVector);
auto request = Azure::Core::Http::Request(
Azure::Core::Http::HttpMethod::Post,
Azure::Core::Url("https://localhost:5001/record/start"),
&postBody);
auto response = m_pipeline->Send(request, Azure::Core::Context::ApplicationContext);
auto& responseHeaders = response->GetHeaders();
auto responseId = responseHeaders.find("x-recording-id");
return Azure::Response<std::string>(responseId->second, std::move(response));
}
Azure::Response<Azure::Core::Http::HttpStatusCode> PostStopRecording(
std::string const& recordingId)
{
auto request = Azure::Core::Http::Request(
Azure::Core::Http::HttpMethod::Post,
Azure::Core::Url("https://localhost:5001/record/stop"));
request.SetHeader("x-recording-id", recordingId);
auto response = m_pipeline->Send(request, Azure::Core::Context::ApplicationContext);
auto responseCode = response->GetStatusCode();
return Azure::Response<Azure::Core::Http::HttpStatusCode>(responseCode, std::move(response));
}
Azure::Response<std::string> PostStartPlayback(std::string const& recordingFile)
{
std::string proxyServerRequest;
proxyServerRequest = "{ \"x-recording-file\": \"";
proxyServerRequest += Azure::Core::Url::Encode(recordingFile);
proxyServerRequest += "\"}";
std::vector<uint8_t> bodyVector{proxyServerRequest.begin(), proxyServerRequest.end()};
Azure::Core::IO::MemoryBodyStream postBody(bodyVector);
auto request = Azure::Core::Http::Request(
Azure::Core::Http::HttpMethod::Post,
Azure::Core::Url("https://localhost:5001/playback/start"),
&postBody);
auto response = m_pipeline->Send(request, Azure::Core::Context::ApplicationContext);
auto& responseHeaders = response->GetHeaders();
auto responseId = responseHeaders.find("x-recording-id");
return Azure::Response<std::string>(responseId->second, std::move(response));
}
Azure::Response<Azure::Core::Http::HttpStatusCode> PostStopPlayback(
std::string const& recordingId)
{
auto request = Azure::Core::Http::Request(
Azure::Core::Http::HttpMethod::Post,
Azure::Core::Url("https://localhost:5001/playback/stop"));
request.SetHeader("x-recording-id", recordingId);
auto response = m_pipeline->Send(request, Azure::Core::Context::ApplicationContext);
auto responseCode = response->GetStatusCode();
return Azure::Response<Azure::Core::Http::HttpStatusCode>(responseCode, std::move(response));
}
Azure::Response<std::string> ProxyServerGetUrl(
std::string const& recordingId,
bool isRecording,
std::string const& urlToRecord)
{
Azure::Core::Url targetUrl{urlToRecord};
auto request = Azure::Core::Http::Request(
Azure::Core::Http::HttpMethod::Get,
Azure::Core::Url("https://localhost:5001/" + targetUrl.GetRelativeUrl()));
request.SetHeader(
"x-recording-upstream-base-uri", targetUrl.GetScheme() + "://" + targetUrl.GetHost());
request.SetHeader("x-recording-id", recordingId);
request.SetHeader("x-recording-mode", (isRecording ? "record" : "playback"));
auto response = m_pipeline->Send(request, Azure::Core::Context::ApplicationContext);
std::string responseBody(response->GetBody().begin(), response->GetBody().end());
return Azure::Response<std::string>(responseBody, std::move(response));
}
Azure::Response<Azure::Core::Http::HttpStatusCode> IsAlive()
{
auto request = Azure::Core::Http::Request(
Azure::Core::Http::HttpMethod::Get,
Azure::Core::Url("https://localhost:5001/Admin/IsAlive"));
auto response = m_pipeline->Send(request, Azure::Core::Context::ApplicationContext);
auto statusCode = response->GetStatusCode();
return Azure::Response<Azure::Core::Http::HttpStatusCode>(statusCode, std::move(response));
}
~TestProxy() {}
};
TEST_F(TransportAdapterOptions, AccessTestProxyServer)
{
TestProxy proxyServer;
EXPECT_EQ(Azure::Core::Http::HttpStatusCode::Ok, proxyServer.IsAlive().Value);
std::string recordingId;
EXPECT_NO_THROW(recordingId = proxyServer.PostStartRecording("testRecording.json").Value);
GTEST_LOG_(INFO) << "Started recording with ID " << recordingId;
std::string response;
EXPECT_NO_THROW(
response
= proxyServer.ProxyServerGetUrl(recordingId, true, AzureSdkHttpbinServer::Get()).Value);
GTEST_LOG_(INFO) << "Response for recording " << recordingId << "is: " << response;
EXPECT_NO_THROW(proxyServer.PostStopRecording(recordingId));
EXPECT_NO_THROW(recordingId = proxyServer.PostStartPlayback("testRecording.json").Value);
GTEST_LOG_(INFO) << "Started playback with ID " << recordingId;
EXPECT_NO_THROW(
response
= proxyServer.ProxyServerGetUrl(recordingId, false, AzureSdkHttpbinServer::Get()).Value);
GTEST_LOG_(INFO) << "Recorded Response for " << recordingId << "is: " << response;
EXPECT_NO_THROW(proxyServer.PostStopPlayback(recordingId));
}
TEST_F(TransportAdapterOptions, TestProxyServerWithInvalidCertificate)
{
TestProxy::TestProxyOptions options;
options.Transport.ExpectedTlsRootCertificate = InvalidTestProxyHttpsCertificate;
TestProxy proxyServer(options);
EXPECT_THROW(proxyServer.IsAlive(), Azure::Core::Http::TransportException);
}
}}} // namespace Azure::Core::Test

View File

@ -94,6 +94,12 @@ stages:
displayName: Verify Proxy Server Working Correctly.
condition: and(succeeded(), variables.RunProxyTests, contains(variables.CmakeArgs, 'BUILD_TESTING=ON'))
# Start the test proxy tool for testing. Only start it when previous steps had succeeded and we're enabling testing.
- template: /eng/common/testproxy/test-proxy-tool.yml
parameters:
runProxy: true
targetVersion: 1.0.0-dev.20220810.2
PostTestSteps:
# Shut down the test server. This uses curl to send a request to the "terminateserver" websocket endpoint.
# When the test server receives a request on terminateserver, it shuts down gracefully.
@ -115,7 +121,7 @@ stages:
docker stop $_ `
}
displayName: Shutdown Squid Proxy.
condition: and(contains(variables.CmakeArgs, 'BUILD_TESTING=ON'), variables.RunProxyTests, contains(variables['OSVmImage'], 'linux'))
condition: and(variables.RunProxyTests, contains(variables.CmakeArgs, 'BUILD_TESTING=ON'), contains(variables['OSVmImage'], 'linux'))
- template: /eng/common/pipelines/templates/steps/publish-artifact.yml
parameters:
ArtifactPath: '$(Build.SourcesDirectory)/WebSocketServer.log'