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
172 changes: 156 additions & 16 deletions src/System.Management.Automation/security/SecurityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

using System;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Management.Automation.Security;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

Expand Down Expand Up @@ -65,6 +67,22 @@ internal enum RunPromptDecision

#region constructor

/// <summary>
/// The EKU OID that identifies a certificate is from Azure Trusted Signing.
/// </summary>
private const string _azureTrustedSigningIdentifier = "1.3.6.1.4.1.311.97.1.0";

/// <summary>
/// The OID prefix that uniquely identifies a certificate issued by Azure Trusted Signing.
/// </summary>
private const string _azureTrustedSigningIdPrefix = "1.3.6.1.4.1.311.97.";

[TraceSource("SecurityManager", "Security Manager Script Trust Checks.")]
private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer(
"SecurityManager",
"Security Manager Script Trust Checks.",
false);

// execution policy that dictates what can run in msh
private ExecutionPolicy _executionPolicy;

Expand Down Expand Up @@ -217,7 +235,7 @@ private bool CheckPolicy(ExternalScriptInfo script, PSHost host, out Exception r
if (signature.Status == SignatureStatus.Valid)
{
// The file is signed by a trusted publisher
if (IsTrustedPublisher(signature, path))
if (IsTrustedPublisher(signature))
{
policyCheckPassed = true;
}
Expand Down Expand Up @@ -287,7 +305,7 @@ private bool CheckPolicy(ExternalScriptInfo script, PSHost host, out Exception r
if (signature.Status == SignatureStatus.Valid)
{
// The file is signed by a trusted publisher
if (IsTrustedPublisher(signature, path))
if (IsTrustedPublisher(signature))
{
policyCheckPassed = true;
}
Expand Down Expand Up @@ -350,7 +368,7 @@ private bool CheckPolicy(ExternalScriptInfo script, PSHost host, out Exception r
// The file is signed by a trusted publisher
if (signature.Status == SignatureStatus.Valid)
{
if (IsTrustedPublisher(signature, path))
if (IsTrustedPublisher(signature))
{
policyCheckPassed = true;
}
Expand Down Expand Up @@ -431,51 +449,173 @@ private static bool IsLocalFile(string filename)
#endif
}

// Checks that a publisher is trusted by the system or is one of
// the signed product binaries
private static bool IsTrustedPublisher(Signature signature, string file)
#nullable enable
/// <summary>
/// Checks if the publisher is trusted by checking whether the
/// certificate thumbprint is in the "Trusted Publishers" store or
/// the Azure Trusted Signer Publisher ID is present in the
/// "Trusted Publishers" store.
/// </summary>
/// <param name="signature">The signature to check.</param>
/// <returns>True if the publisher is trusted.</returns>
private static bool IsTrustedPublisher(Signature signature)
{
// Get the thumbprint of the current signature
X509Certificate2 signerCertificate = signature.SignerCertificate;
string thumbprint = signerCertificate.Thumbprint;
s_tracer.WriteLine("Checking if publisher with thumbprint {0} is trusted.", thumbprint);

TryGetAzureTrustedSignerPublisherId(signerCertificate, out string? azurePublisherId);

// See if it matches any in the list of trusted publishers
X509Store trustedPublishers = new X509Store(StoreName.TrustedPublisher, StoreLocation.CurrentUser);
trustedPublishers.Open(OpenFlags.ReadOnly);

bool isTrusted = false;
foreach (X509Certificate2 trustedCertificate in trustedPublishers.Certificates)
{
s_tracer.WriteLine("Checking publisher against certificate '{0}' and thumbprint {1}.",
trustedCertificate.FriendlyName,
trustedCertificate.Thumbprint);

if (string.Equals(trustedCertificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
{
if (!IsUntrustedPublisher(signature, file))
{
return true;
}
isTrusted = true;
}
else if (azurePublisherId is not null &&
TryGetAzureTrustedSignerPublisherId(trustedCertificate, out string? trustedIdentifier) &&
azurePublisherId == trustedIdentifier)
{
isTrusted = true;
break;
Copy link

Copilot AI Aug 11, 2025

Choose a reason for hiding this comment

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

The break statement exits the loop early when an Azure publisher ID match is found, but the thumbprint check continues through all certificates. For consistency and performance, consider adding a break after setting isTrusted = true for the thumbprint match as well.

Copilot uses AI. Check for mistakes.
}
}

// Do a final check to verify that the certificate has not been
// explicitly added to the "Disallowed" store.
if (isTrusted && !IsUntrustedPublisher(signerCertificate))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd expect IsUntrustedPublisher() on top of the method - it makes no sense to check trust if certificate is blocked.
Also it would be nice to add the check in trace too.

{
return true;
}

return false;
}

private static bool IsUntrustedPublisher(Signature signature, string file)
/// <summary>
/// Checks if the publisher is untrusted by checking whether the same
/// certificate thumbprint is in the "Disallowed" store.
/// </summary>
/// <param name="signerCertificate">The certificate to check by thumbprint.</param>
/// <returns>True when the publisher is untrusted.</returns>
private static bool IsUntrustedPublisher(X509Certificate2 signerCertificate)
{
// Get the thumbprint of the current signature
X509Certificate2 signerCertificate = signature.SignerCertificate;
string thumbprint = signerCertificate.Thumbprint;
s_tracer.WriteLine("Checking if certificate {0} is untrusted.",
thumbprint);

// See if it matches any in the list of trusted publishers
X509Store trustedPublishers = new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser);
trustedPublishers.Open(OpenFlags.ReadOnly);
X509Store untrustedPublishers = new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser);
untrustedPublishers.Open(OpenFlags.ReadOnly);

foreach (X509Certificate2 trustedCertificate in trustedPublishers.Certificates)
foreach (X509Certificate2 untrustedCertificate in untrustedPublishers.Certificates)
{
if (string.Equals(trustedCertificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
s_tracer.WriteLine("Checking publisher against untrusted certificate '{0}' and thumbprint {1}.",
untrustedCertificate.FriendlyName,
untrustedCertificate.Thumbprint);

if (string.Equals(untrustedCertificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

/// <summary>
/// Checks if the certificate has the Azure Trusted Signer Publisher ID
/// EKU present and sets publisherId to that unique identifier.
/// </summary>
/// <param name="certificate">The certificate to check.</param>
/// <param name="publisherId">An opaque blob that uniquely identifies the publisher if present.</param>
/// <returns>True when the certificate has the Azure Trusted Signer Publisher ID EKU.</returns>
private static bool TryGetAzureTrustedSignerPublisherId(
X509Certificate2 certificate,
[NotNullWhen(true)] out string? publisherId)
{
bool containsAzTSIdentifier = false;
string? azurePubOid = null;

foreach (X509Extension ext in certificate.Extensions)
{
if (ext is X509EnhancedKeyUsageExtension ekuExt)
{
// The EKU OIDs need to contain the Azure Trusted Signing Identifier
// and have one that starts with the Azure Trusted Signing ID Prefix.
foreach (Oid oid in ekuExt.EnhancedKeyUsages)
{
if (oid.Value == _azureTrustedSigningIdentifier)
{
containsAzTSIdentifier = true;
}
else if (oid.Value?.StartsWith(_azureTrustedSigningIdPrefix) == true)
{
azurePubOid = oid.Value;
}
}

break; // No need to check other extensions.
}
}

string? caThumbprint = null;
if (containsAzTSIdentifier && azurePubOid is not null)
{
s_tracer.WriteLine("Certificate {0} has Azure Trusted Signer EKU OID {1}.",
certificate.Thumbprint,
azurePubOid);

// To avoid matching on certs that have the same EKU OID added
// we add the thumbprint of the root CA to the unique
// identifier. This means someone can't manually create a
// cert with the same OID as one already trusted as it needs to
// come from the same CA. We don't do a revocation check as we
// aren't checking the validity of the certificate, just getting
// the thumbprint of the root CA.
using X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
if (chain.Build(certificate))
{
// Remarks state that the last element in the chain in the
// root CA on all platforms.
caThumbprint = chain.ChainElements[^1].Certificate.Thumbprint;
}
else
{
s_tracer.WriteLine("Failed to find root CA for certificate {0}: {1}",
certificate.Thumbprint,
chain.ChainStatus[0].StatusInformation);
}
}

if (caThumbprint is not null)
{
publisherId = $"{azurePubOid}.{caThumbprint}";

s_tracer.WriteLine("Publisher ID for certificate {0} is {1}.",
certificate.Thumbprint,
publisherId);
return true;
}
else
{
publisherId = null;
return false;
}
}
#nullable disable

/// <summary>
/// Trust a publisher by adding it to the "Trusted Publishers" store.
/// </summary>
Expand Down
Loading
Loading