Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions src/AppInstallerCLICore/Commands/DebugCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
#include <winrt/Microsoft.Management.Configuration.h>
#include <winrt/Microsoft.Management.Configuration.SetProcessorFactory.h>
#include "AppInstallerDownloader.h"
#include <AppInstallerErrors.h>
#include "Sixel.h"
#include <winget/Certificates.h>
#include <winget/HttpClientHelper.h>
#include <winget/RepositorySource.h>

using namespace AppInstaller::CLI::Execution;

Expand Down Expand Up @@ -68,6 +71,7 @@ namespace AppInstaller::CLI
std::make_unique<GetSignerCommand>(FullName()),
std::make_unique<LogViewerTestCommand>(FullName()),
std::make_unique<DebugDscResourceCommand>(FullName()),
std::make_unique<ValidateStorePinningCommand>(FullName()),
});
}

Expand Down Expand Up @@ -708,6 +712,168 @@ namespace AppInstaller::CLI
context.Reporter.Info() << std::endl;
}
}

// ── ValidateStorePinningCommand ──────────────────────────────────────────────

namespace
{
std::string PercentageToString(double value)
{
std::ostringstream stream;
stream << std::fixed << std::setprecision(1) << (value * 100.0) << '%';
return std::move(stream).str();
}

// The state shared with the certificate validation callback, which is invoked on another thread.
struct StorePinningValidationState
{
std::mutex Lock;
bool CertificateSeen = false;
std::string ChainDescription;
bool PinningAccepted = false;
HRESULT Error = S_OK;
};
}

std::vector<Argument> ValidateStorePinningCommand::GetArguments() const
{
return {
Argument{ "url", 'u', Args::Type::SourceArg, Resource::String::SourceListUpdatedNever, ArgumentType::Positional },
};
}

Resource::LocString ValidateStorePinningCommand::ShortDescription() const
{
return Utility::LocIndString("Validate the Store source certificate pinning"sv);
}

Resource::LocString ValidateStorePinningCommand::LongDescription() const
{
return Utility::LocIndString(
"Connects to the given URL (defaulting to the Microsoft Store source URL) and validates the "
"certificate that it presents against the current static pinning configuration for the Store source. "
"Use this to determine if a rotated certificate will be accepted before it is deployed."sv);
}

void ValidateStorePinningCommand::ExecuteInternal(Execution::Context& context) const
{
Repository::SourceDetails storeDetails = Repository::GetWellKnownSourceDetails(Repository::WellKnownSource::MicrosoftStore);

std::string url{ storeDetails.Arg };
if (context.Args.Contains(Args::Type::SourceArg))
{
url = context.Args.GetArg(Args::Type::SourceArg);
}

if (url.find("://") == std::string::npos)
{
url = "https://" + url;
}

context.Reporter.Info() << "Validating the Microsoft Store source pinning configuration against: " << url << std::endl;

if (storeDetails.CertificatePinningConfiguration.IsEmpty())
{
context.Reporter.Warn() <<
"The Store source has no pinning configuration; it has likely been disabled by the "
"BypassCertificatePinningForMicrosoftStore admin setting. Every certificate will be accepted." << std::endl;
}
else
{
context.Reporter.Info() << "Current pinning configuration:" << std::endl <<
storeDetails.CertificatePinningConfiguration.GetDescription() << std::endl;
context.Reporter.Info() << "Pinned certificate remaining lifetime: " <<
PercentageToString(storeDetails.CertificatePinningConfiguration.GetRemainingLifetimePercentage()) << std::endl;
}

auto state = std::make_shared<StorePinningValidationState>();

// Accept every certificate so that the pinning result can be reported independently of the connection result.
Certificates::PinningConfiguration captureConfiguration{ "Store Pinning Validation" };
captureConfiguration.AddChain(std::make_shared<Certificates::CallbackPinningChainValidation>(
[state, pinningConfiguration = storeDetails.CertificatePinningConfiguration](PCCERT_CONTEXT certContext)
{
std::lock_guard<std::mutex> lock{ state->Lock };
state->CertificateSeen = true;

try
{
wil::unique_cert_chain_context chainContext;

try
{
chainContext = Certificates::PinningConfiguration::BuildCertificateChain(certContext);
}
catch (...)
{
// Revocation information may not be reachable for all endpoints; try again without it.
LOG_CAUGHT_EXCEPTION();
chainContext = Certificates::PinningConfiguration::BuildCertificateChain(certContext, nullptr, nullptr, 0);
}

state->ChainDescription = Certificates::GetCertificateChainDescription(chainContext.get());
state->PinningAccepted = pinningConfiguration.Validate(certContext, chainContext.get());
}
catch (...)
{
state->Error = LOG_CAUGHT_EXCEPTION();
}

return true;
}));

Http::HttpClientHelper client;
client.SetPinningConfiguration(captureConfiguration, context.GetSharedThreadGlobals());

HRESULT connectionResult = S_OK;
std::optional<web::http::status_code> statusCode;

try
{
statusCode = client.Get(Utility::ConvertToUTF16(url)).get().status_code();
}
catch (...)
{
connectionResult = LOG_CAUGHT_EXCEPTION();
}

if (state->CertificateSeen)
{
context.Reporter.Info() << "Server certificate chain (root first):" << std::endl << state->ChainDescription << std::endl;
}

if (statusCode)
{
context.Reporter.Info() << "Connection completed with HTTP status: " << statusCode.value() << std::endl;
}
else
{
context.Reporter.Warn() << "Connection failed with: 0x" << Logging::SetHRFormat << connectionResult << std::endl;
}

if (!state->CertificateSeen)
{
context.Reporter.Error() << "No server certificate was received; the endpoint could not be reached." << std::endl;
AICLI_TERMINATE_CONTEXT(FAILED(connectionResult) ? connectionResult : E_UNEXPECTED);
}

if (FAILED(state->Error))
{
context.Reporter.Error() << "Failed to evaluate the certificate against the pinning configuration: 0x" <<
Logging::SetHRFormat << state->Error << std::endl;
AICLI_TERMINATE_CONTEXT(state->Error);
}

if (state->PinningAccepted)
{
context.Reporter.Info() << "Pinning validation: PASSED" << std::endl;
}
else
{
context.Reporter.Error() << "Pinning validation: FAILED" << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH);
}
}
}

#endif
14 changes: 14 additions & 0 deletions src/AppInstallerCLICore/Commands/DebugCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ namespace AppInstaller::CLI
protected:
void ExecuteInternal(Execution::Context& context) const override;
};

// Validates the Microsoft Store source certificate pinning configuration against an endpoint.
struct ValidateStorePinningCommand final : public Command
{
ValidateStorePinningCommand(std::string_view parent) : Command("validate-store-pinning", {}, parent) {}

std::vector<Argument> GetArguments() const override;

Resource::LocString ShortDescription() const override;
Resource::LocString LongDescription() const override;

protected:
void ExecuteInternal(Execution::Context& context) const override;
};
}

#endif
9 changes: 9 additions & 0 deletions src/AppInstallerCLICore/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@
#include <wil/registry_helpers.h>
#pragma warning( pop )

#ifndef WINGET_DISABLE_FOR_FUZZING
#pragma warning( push )
#pragma warning ( disable : 26495 26439 )
#include <cpprest/http_client.h>
#include <cpprest/json.h>
#include <cpprest/uri_builder.h>
#pragma warning( pop )
#endif

#include <wrl/client.h>
#include <wrl/implements.h>
#include <AppxPackaging.h>
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ namespace AppInstaller::Repository
// Check if a source matches a well known source
std::optional<WellKnownSource> CheckForWellKnownSource(const SourceDetails& sourceDetails);

// Gets the details for a well known source, including its certificate pinning configuration.
// The details are not populated with any locally stored metadata.
SourceDetails GetWellKnownSourceDetails(WellKnownSource source);

// Individual source agreement entry. Label will be highlighted in the display as the key of the agreement entry.
struct SourceAgreement
{
Expand Down
5 changes: 5 additions & 0 deletions src/AppInstallerRepositoryCore/RepositorySource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,11 @@ namespace AppInstaller::Repository
return CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type);
}

SourceDetails GetWellKnownSourceDetails(WellKnownSource source)
{
return GetWellKnownSourceDetailsInternal(source);
}

Source::Source() {}

Source::Source(std::string_view name)
Expand Down
28 changes: 28 additions & 0 deletions src/AppInstallerSharedLib/Certificates.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,34 @@ namespace AppInstaller::Certificates
return result;
}

std::string PinningConfiguration::GetDescription() const
{
std::ostringstream stream;
stream << "Pinning configuration [" << m_identifier << "]";

if (m_configuration.empty())
{
stream << ": <no pinning configured>";
}
else
{
size_t index = 0;

for (const auto& chain : m_configuration)
{
stream << std::endl << "Chain #" << ++index << ':' << std::endl << chain->GetDescription();
}
}

return std::move(stream).str();
}

std::string GetCertificateChainDescription(PCCERT_CHAIN_CONTEXT chainContext)
{
THROW_HR_IF(E_INVALIDARG, !chainContext || chainContext->cChain == 0);
return GetDescriptionOfCertChain(chainContext);
}

std::string GetAuthenticodeSubject(const std::filesystem::path& filePath)
{
const std::wstring& pathStr = filePath.wstring();
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerSharedLib/Public/winget/Certificates.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ namespace AppInstaller::Certificates
// Returns an empty string if the file is unsigned, untrusted, or on any error.
std::string GetAuthenticodeSubject(const std::filesystem::path& filePath);

// Gets a human readable, indented description of the certificates in the given chain, from root to leaf.
std::string GetCertificateChainDescription(PCCERT_CHAIN_CONTEXT chainContext);

// Defines the types of certificate pinning to perform.
enum class PinningVerificationType : uint32_t
{
Expand Down Expand Up @@ -254,6 +257,9 @@ namespace AppInstaller::Certificates
// Loads the pinning configuration from the given JSON.
[[nodiscard]] bool LoadFrom(const Json::Value& configuration);

// Gets a description of the configuration, including all of its chains.
std::string GetDescription() const;

// Determines how far the configuration is through its lifespan (the maximum of all of its chains).
double GetRemainingLifetimePercentage() const;

Expand Down
Loading