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

Simplify and improve signature verification for workloads on Windows #36787

Merged
merged 16 commits into from
Jan 4, 2024

Conversation

joeloff
Copy link
Member

@joeloff joeloff commented Nov 10, 2023

Description

Improve code signing verification on Windows for workloads (MSI based installs).

  • Add revocation checks when calling WinVerifyTrust. Revocation checks default to online, except if the AllowOnlineRevocationChecks policy key is set to 0. The policy setting can be used to perform revocation checks when network access is restricted.
  • Validate that the certificate chain terminates in a trusted Microsoft root (certificate pinning).

This change also introduces the use of Microsoft.Windows.CsWin32 to generate all the native structs and API calls. As a result, we can safely remove a chunk of code and rely on the generated source code instead.

@joeloff joeloff requested review from a team and dtivel November 13, 2023 21:24
@joeloff joeloff marked this pull request as ready for review November 13, 2023 21:25
@joeloff
Copy link
Member Author

joeloff commented Nov 13, 2023

@dtivel would you mind looking at the WinVerifyTrust and X509 extension methods?

I'm still working on fixing up Source Build

src/Cli/dotnet/NativeMethods.txt Outdated Show resolved Hide resolved

bool isTrustedOrganization = AuthentiCode.IsSignedByTrustedOrganization(msiPath, AuthentiCode.TrustedOrganizations);
// We know there's a signature, so we can perform additional checks.
X509Certificate certificate = X509Certificate.CreateFromSignedFile(msiPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrap with using statement?

CERT_CHAIN_POLICY_STATUS policyStatus = default;

using X509Chain chain = new();
bool buildResult = chain.Build(new X509Certificate2(certificate));
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here. Either pass certificate to `X509Chain.Build(...) or dispose of the new certificate.

Also, why ignore buildResult? X509Chain.Build(...) could succeed (i.e.: buildResult == true) or it could fail with a variety of states (per X509Chain.ChainStatus), some of which may be recoverable, some not. For example, if the certificate has expired but you have a valid, trusted timestamp, then you may want to ignore X509ChainStatusFlags.NotTimeValid. There are a myriad of other flags you probably don't want to ignore.

Copy link
Member Author

Choose a reason for hiding this comment

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

My thinking was that at this point I would have verified everything I wanted. I was mainly relying on this to create the context pointer for me so I could call into CertVerify... API. But given the issues you mentioned I'll have to restructure this a bit differently, otherwise I need to keep building the same chain and same checks in multiple places.

Copy link
Member Author

Choose a reason for hiding this comment

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

As per the new iteration, we'll build the chain using the certificate handle and avoid using the X509Chain.Build() so we don't recheck policies covered by WinTrust

src/Cli/dotnet/commands/dotnet-workload/SignCheck.cs Outdated Show resolved Hide resolved
trustData[0].dwUIChoice = WINTRUST_DATA_UICHOICE.WTD_UI_NONE;
trustData[0].fdwRevocationChecks = WINTRUST_DATA_REVOCATION_CHECKS.WTD_REVOKE_WHOLECHAIN;
trustData[0].dwUnionChoice = WINTRUST_DATA_UNION_CHOICE.WTD_CHOICE_FILE;
trustData[0].dwStateAction = WINTRUST_DATA_STATE_ACTION.WTD_STATEACTION_VERIFY;
Copy link
Contributor

Choose a reason for hiding this comment

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

Docs say:

Value Meaning
WTD_STATEACTION_IGNORE 0x00000000 Ignore the hWVTStateData member.
WTD_STATEACTION_VERIFY 0x00000001 Verify the trust of the object (typically a file) that is specified by the dwUnionChoice member. The hWVTStateData member will receive a handle to the state data. This handle must be freed by specifying the WTD_STATEACTION_CLOSE action in a subsequent call.
WTD_STATEACTION_CLOSE 0x00000002 Free the hWVTStateData member previously allocated with the WTD_STATEACTION_VERIFY action. This action must be specified for every use of the WTD_STATEACTION_VERIFY action.

This sample shows proper use of WTD_STATEACTION_VERIFY + WTD_STATEACTION_CLOSE.

I don't see a call with WTD_STATEACTION_CLOSE anywhere. Should you use WTD_STATEACTION_IGNORE instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, I think I need to just make the release call to free up the handle.

Copy link
Member Author

Choose a reason for hiding this comment

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

This has been fixed

/// <param name="path">The path of the file to verify.</param>
/// <param name="cacheOnlyRevocationChecks">Prevents revocation checks over the network when set to <see langword="true"/>.</param>
/// <returns>0 if successful.</returns>
public static unsafe int IsSigned(string path, bool cacheOnlyRevocationChecks = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

cacheOnlyRevocationChecks is poorly named, as it implies that only revocation checks will be cached when true. (Windows may cache other things, like intermediate certificate authority certificates.) How about allowOnlineRevocationChecks?

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, the suggested name is more clear about the intent.

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed the policy key to AllowOnlineRevocationChecks

var certificates = AuthentiCode.GetCertificates(Assembly.GetExecutingAssembly().Location);

Assert.Empty(certificates);
X509Certificate certificate = X509Certificate.CreateFromSignedFile(Path.Combine(s_testDataPath, file));
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrap with using statement.

Copy link
Member Author

Choose a reason for hiding this comment

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

Most X509 pieces are removed in the new iteration, but I did add usings for the few that remain

@joeloff
Copy link
Member Author

joeloff commented Dec 7, 2023

I'm still waiting on updated runtime-asset packages for the tests. Locally they pass, but until the new package is available in the feed, they'll fail in the PR build.

@joeloff joeloff requested a review from bartonjs December 7, 2023 23:28
src/Cli/dotnet/Installer/Windows/Security/Signature.cs Outdated Show resolved Hide resolved
throw new CryptographicException(string.Format(LocalizableStrings.UnableToCheckCertificateChainPolicy, nameof(CERT_CHAIN_POLICY_MICROSOFT_ROOT)));
}

CertFreeCertificateChain(pChainContext);
Copy link
Member

Choose a reason for hiding this comment

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

CertFreeCertificateChain has three manual calls in this routine. I recommend wrapping lines 43-64 in a try/finally and just making the call once, in the finally.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in the latest commit

src/Cli/dotnet/Installer/Windows/Security/Signature.cs Outdated Show resolved Hide resolved
Comment on lines 107 to 108
trustData[0].dwUIContext = 0;
trustData[0].Anonymous.pFile = fileInfo;
Copy link
Member

Choose a reason for hiding this comment

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

I'd move these up to be with the rest of the unconditional assignments.

internal static unsafe int HasMicrosoftTrustedRoot(string path)
{
// Create an X509Certificate2 instance so we can access the certificate context and create a chain context.
using X509Certificate2 certificate = new(path);
Copy link
Member

@bartonjs bartonjs Jan 4, 2024

Choose a reason for hiding this comment

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

I feel like WinVerifyTrust already exposes back the certificate, and this has to replicate that work. I advise checking to see if you can already get this from trustData->pCert. If so, then between the action call to WinVerifyTrust and the cleanup call, do PCCERT_CONTEXT pCertContext = CertDuplicateCertificateContext(trustData->pCert->psCertContext);, and pass the PCCERT_CONTEXT (or CERT_CONTEXT*, I suppose) into this method instead of string path.

It's both more performant, and would avoid a theoretical ToC/ToU problem if the file isn't locked during your call.

(Don't forget to CertFreeCertificateContext the value after 😄)

Copy link
Member Author

@joeloff joeloff Jan 4, 2024

Choose a reason for hiding this comment

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

I don't think I have access to it. Since I'm verifying a file, the union struct is a WINTRUT_FILE_INFO* and not a WINTRUST_CERT_INFO*. In this case, the WinVerifyTrust call creates the cert context from the file, but won't expose that back to me.

I suppose I could do it the other way around, extract the cert context from the file, then let WinVerifyTrust verify the cert context, but I'll need to check if the OS does anything else when asked to verify a file. Then I could do dwUnionChoice = WINTRUST_DATA_UNION_CHOICE.WTD_CHOICE_CERT instead of dwUnionChoice = WINTRUST_DATA_UNION_CHOICE.WTD_CHOICE_FILE

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I thought that some of the other data got populated by calling into WinVerifyTrust; and had glossed over it being a union type.

So, disregard.

@richlander
Copy link
Member

What will this feature do in airgapped environments? Are we assuming that AllowOnlineRevocationChecks policy key is set to 0.

I'm assuming that this feature will create a gap with Linux. What's the thinking on that?

@joeloff
Copy link
Member Author

joeloff commented Jan 4, 2024

What will this feature do in airgapped environments? Are we assuming that AllowOnlineRevocationChecks policy key is set to 0.

I'm assuming that this feature will create a gap with Linux. What's the thinking on that?

This is specific to Windows only as it's only used for verifying the signatures for the MSI packages. The policy flag will force the checks to use the cached certificate revocation list in the machine trust store.

@baronfel
Copy link
Member

baronfel commented Jan 4, 2024

@richlander for some context, on non-Windows platforms we use the so-called 'file based' workload installation, which essentially just downloads a NuGet package and extracts its contents to a specific folder structure. In these scenarios we should be making use of NuGet package signing when that is enabled on each platform. This MSI signature verification is something we can do for MSI-based workload installations, which are Windows-only. When we implement package-manager-based workloads for e.g. Linux platforms, we could potentially implement similar signature checks.

joeloff and others added 3 commits January 4, 2024 08:33
PR feedback

Co-authored-by: Jeremy Barton <jbarton@microsoft.com>
PR feedback

Co-authored-by: Jeremy Barton <jbarton@microsoft.com>
@joeloff joeloff requested a review from a team as a code owner January 4, 2024 17:56
@joeloff joeloff enabled auto-merge January 4, 2024 18:57
@joeloff joeloff merged commit 7784d76 into dotnet:main Jan 4, 2024
16 checks passed
@joeloff
Copy link
Member Author

joeloff commented Jan 4, 2024

/backport to release/8.0.1xx

Copy link
Contributor

github-actions bot commented Jan 4, 2024

Started backporting to release/8.0.1xx: https://github.com/dotnet/sdk/actions/runs/7415512861

Copy link
Contributor

github-actions bot commented Jan 4, 2024

@joeloff backporting to release/8.0.1xx failed, the patch most likely resulted in conflicts:

$ git am --3way --ignore-whitespace --keep-non-patch changes.patch

Applying: Preliminary code change
.git/rebase-apply/patch:401: trailing whitespace.
                return WinVerifyTrust((HWND)IntPtr.Zero, ref policyGuid, trustData);                
.git/rebase-apply/patch:404: trailing whitespace.
        }        
.git/rebase-apply/patch:1109: trailing whitespace.
        /// Determines whether the certificate is intended for code signing. 
warning: 3 lines add whitespace errors.
Using index info to reconstruct a base tree...
M	eng/Versions.props
M	src/Cli/dotnet/Installer/Windows/MsiPackageCache.cs
M	src/Cli/dotnet/Installer/Windows/Security/NativeMethods.cs
M	src/Cli/dotnet/Installer/Windows/Security/WinTrustData.cs
M	src/Cli/dotnet/commands/dotnet-workload/SignCheck.cs
M	src/Cli/dotnet/commands/dotnet-workload/install/NetSdkMsiInstallerClient.cs
M	src/Cli/dotnet/dotnet.csproj
Falling back to patching base and 3-way merge...
Auto-merging src/Cli/dotnet/dotnet.csproj
Auto-merging src/Cli/dotnet/commands/dotnet-workload/install/NetSdkMsiInstallerClient.cs
Auto-merging src/Cli/dotnet/commands/dotnet-workload/SignCheck.cs
Removing src/Cli/dotnet/Installer/Windows/Security/WinTrustFileInfo.cs
CONFLICT (modify/delete): src/Cli/dotnet/Installer/Windows/Security/WinTrustData.cs deleted in Preliminary code change and modified in HEAD. Version HEAD of src/Cli/dotnet/Installer/Windows/Security/WinTrustData.cs left in tree.
Removing src/Cli/dotnet/Installer/Windows/Security/UnionChoice.cs
Removing src/Cli/dotnet/Installer/Windows/Security/UIChoice.cs
Removing src/Cli/dotnet/Installer/Windows/Security/StateAction.cs
Removing src/Cli/dotnet/Installer/Windows/Security/RevocationChecks.cs
Removing src/Cli/dotnet/Installer/Windows/Security/ProviderSettings.cs
CONFLICT (modify/delete): src/Cli/dotnet/Installer/Windows/Security/NativeMethods.cs deleted in Preliminary code change and modified in HEAD. Version HEAD of src/Cli/dotnet/Installer/Windows/Security/NativeMethods.cs left in tree.
Removing src/Cli/dotnet/Installer/Windows/Security/CryptQueryObjectType.cs
Removing src/Cli/dotnet/Installer/Windows/Security/Crypt32.cs
Removing src/Cli/dotnet/Installer/Windows/Security/CertQueryFormatFlags.cs
Removing src/Cli/dotnet/Installer/Windows/Security/CertQueryContentFlags.cs
Auto-merging src/Cli/dotnet/Installer/Windows/MsiPackageCache.cs
Auto-merging eng/Versions.props
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
Patch failed at 0001 Preliminary code change
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
Error: The process '/usr/bin/git' failed with exit code 128

Please backport manually!

Copy link
Contributor

github-actions bot commented Jan 4, 2024

@joeloff an error occurred while backporting to release/8.0.1xx, please check the run log for details!

Error: git am failed, most likely due to a merge conflict.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Workloads untriaged Request triage from a team member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants