Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server certificate pinning for Store source #2347

Merged
merged 20 commits into from
Jul 28, 2022

Conversation

JohnMcPMS
Copy link
Member

@JohnMcPMS JohnMcPMS commented Jul 19, 2022

Change

This change adds a generic certificate chain verification infrastructure for pinning certificate chains. It is specifically used to pin the Microsoft Store source by default. More sources may be pinned later, but currently the packaged index is less in need of it because it is already signed.

The pinning configuration consists of 1 or more chains, only one of which needs to successfully validate the incoming certificate. This allows for rolling to a new certificate when needed. Each chain consists of a fixed set of certificates, which can each be configured to validate any or all of the following properties:

  • Public Key
  • Subject
  • Issuer

If the certificate is configured to validate none of the values, it will allow any certificate through.

An admin setting is added to disable pinning, both as an emergency measure in the event that there is a bug or rolled certificate that was not communicated, but also because there are test scenarios where the user actively wants to disable it (HTTPS redirection via something like Fiddler).

The configuration can be loaded from JSON for future dynamic configuration, but it is currently only as a test hook to enable configuration via Group Policy.

In order to better secure the source by default, reconfiguring (remove then add) the Store source manually will convert it back to the built-in values. This includes the pinning configuration.

It was necessary to modify the cpprestsdk subtree to add a new callback. This enables the request handle to be passed back to our code when the server certificate is first available. We can then check the server certificate against the configured pinning chain, making a decision to terminate the request before it is sent.

Validation

Unit tests are added for the various utilities and the primary pinning configuration/validation.
An E2E test is added for a negative case with the Store, but more could be done to test REST in general in our CI pipeline.

Microsoft Reviewers: Open in CodeFlow

@JohnMcPMS JohnMcPMS requested a review from a team as a code owner July 19, 2022 04:03
@@ -0,0 +1,14 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by WinGetSchemas.rc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WinGetSchemas.rc

nit: CertificateResources.rc

@@ -2039,6 +2039,18 @@ class winhttp_client final : public _http_client_communicator
case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST:
{
p_request_context->on_send_request_validate_cn();

try
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try

Are we directly modifying subtree modules?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upstream codebase is in maintenance mode. We can try to get this change in there but it seems unlikely, and very unlikely to be able to do so in the time frame we need.

PolicyState localManifestFilesPolicy = GroupPolicies().GetState(TogglePolicy::Policy::LocalManifestFiles);
if (localManifestFilesPolicy != PolicyState::NotConfigured)
{
return (localManifestFilesPolicy == PolicyState::Enabled ? true : false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return (localManifestFilesPolicy == PolicyState::Enabled ? true : false);

nit: return localManifestFilesPolicy == PolicyState::Enabled

@@ -196,6 +196,19 @@ namespace AppInstaller::Settings
return std::nullopt;
}

#ifndef AICLI_DISABLE_TEST_HOOKS
// Enable certificate pinning configuration through GP sources for testing
std::string pinningConfigurationName = "CertificatePinning";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string

nit const

@@ -196,6 +196,19 @@ namespace AppInstaller::Settings
return std::nullopt;
}

#ifndef AICLI_DISABLE_TEST_HOOKS
Copy link
Contributor

@yao-msft yao-msft Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AICLI_DISABLE_TEST_HOOKS

Are we planning to expose cert pinning config in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, but I want that to be an explicit decision, not part of this change.

return *this;
}

bool PinningDetails::LoadFrom(const Json::Value& configuration)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PinningDetails::LoadFrom

A comment with sample configuration json would better help with reading the parsing logic


bool PinningDetails::LoadFrom(const Json::Value& configuration)
{
std::string validationName = "Validation";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string

nit: const or declare above for all the json node names across this function?

&m_certificateContext.get()->pCertInfo->SubjectPublicKeyInfo,
&certContext->pCertInfo->SubjectPublicKeyInfo))
{
AICLI_LOG(Core, Verbose, << "Public key mismatch: Expected certificate [" << GetSimpleDisplayName(m_certificateContext.get()) << "], Actual certificate [" << GetSimpleDisplayName(certContext) << "]");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verbose

Should these logging levels at least warning?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose not to log them by default for the case of multiple chains. It is expected that only one of the chains would be valid for any given connection, so logging "Hey there is seemingly a security issue over here!" is not warranted when it turns out that it is actually just the one not being used.

PinningConfiguration::Validate, which is the expected entry point for any non-test use case, does log at Error on a failure to find any good chain. If that is happening, then the user can manually gather a verbose log.

if (m_chain.empty())
{
// An empty chain rejects all inputs.
AICLI_LOG(Core, Verbose, << "Empty pinning chain blindly rejecting chain context");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verbose

I think logging level of this function should be revisited?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this line specifically to Warning as an empty chain is not an expected use case.


std::wostringstream pinningConfig;
pinningConfig << LR"({
"Chains": [{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: seems bad indentation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a raw string, but I can bring the beginning down to make it look less awkward.

Copy link
Contributor

@yao-msft yao-msft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@JohnMcPMS
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants