Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ protected override Signature PerformAction(string filePath)
/// </returns>
protected override Signature PerformAction(string sourcePathOrExtension, byte[] content)
{
return SignatureHelper.GetSignature(sourcePathOrExtension, System.Text.Encoding.Unicode.GetString(content));
return SignatureHelper.GetSignature(sourcePathOrExtension, content);
}
}

Expand Down
27 changes: 15 additions & 12 deletions src/System.Management.Automation/security/Authenticode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ internal static Signature SignFile(SigningOption option,
/// <exception cref="System.IO.FileNotFoundException">
/// Thrown if the file specified by argument fileName is not found.
/// </exception>
internal static Signature GetSignature(string fileName, string fileContent)
internal static Signature GetSignature(string fileName, byte[] fileContent)
{
Signature signature = null;

Expand Down Expand Up @@ -398,7 +398,7 @@ private static uint GetErrorFromSignatureState(SignatureState signatureState)
}
#endif

private static Signature GetSignatureFromWinVerifyTrust(string fileName, string fileContent)
private static Signature GetSignatureFromWinVerifyTrust(string fileName, byte[] fileContent)
{
Signature signature = null;

Expand All @@ -409,6 +409,7 @@ private static Signature GetSignatureFromWinVerifyTrust(string fileName, string
{
Utils.CheckArgForNullOrEmpty(fileName, "fileName");
SecuritySupport.CheckIfFileExists(fileName);

// SecurityUtils.CheckIfFileSmallerThan4Bytes(fileName);
}

Expand All @@ -424,7 +425,8 @@ private static Signature GetSignatureFromWinVerifyTrust(string fileName, string
signature = GetSignatureFromWintrustData(fileName, error, wtd);

wtd.dwStateAction = WinTrustAction.WTD_STATEACTION_CLOSE;
error = WinTrustMethods.WinVerifyTrust(IntPtr.Zero,
error = WinTrustMethods.WinVerifyTrust(
IntPtr.Zero,
ref WINTRUST_ACTION_GENERIC_VERIFY_V2,
ref wtd);

Expand All @@ -441,8 +443,10 @@ private static Signature GetSignatureFromWinVerifyTrust(string fileName, string
return signature;
}

private static uint GetWinTrustData(string fileName, string fileContent,
out WinTrustMethods.WINTRUST_DATA wtData)
private static uint GetWinTrustData(
string fileName,
byte[] fileContent,
out WinTrustMethods.WINTRUST_DATA wtData)
{
wtData = new()
{
Expand All @@ -451,9 +455,6 @@ private static uint GetWinTrustData(string fileName, string fileContent,
dwStateAction = WinTrustAction.WTD_STATEACTION_VERIFY,
};

byte[] contentBytes = fileContent == null
? Array.Empty<byte>()
: System.Text.Encoding.Unicode.GetBytes(fileContent);
unsafe
{
fixed (char* fileNamePtr = fileName)
Expand All @@ -468,26 +469,28 @@ private static uint GetWinTrustData(string fileName, string fileContent,
wtData.dwUnionChoice = WinTrustUnionChoice.WTD_CHOICE_FILE;
wtData.pChoice = &wfi;

return WinTrustMethods.WinVerifyTrust(IntPtr.Zero,
return WinTrustMethods.WinVerifyTrust(
IntPtr.Zero,
ref WINTRUST_ACTION_GENERIC_VERIFY_V2,
ref wtData);
}

fixed (byte* contentPtr = contentBytes)
fixed (byte* contentPtr = fileContent)
{
Guid pwshSIP = new("603BCC1F-4B59-4E08-B724-D2C6297EF351");
WinTrustMethods.WINTRUST_BLOB_INFO wbi = new()
{
cbStruct = (uint)Marshal.SizeOf<WinTrustMethods.WINTRUST_BLOB_INFO>(),
gSubject = pwshSIP,
pcwszDisplayName = fileNamePtr,
cbMemObject = (uint)contentBytes.Length,
cbMemObject = (uint)fileContent.Length,
pbMemObject = contentPtr,
};
wtData.dwUnionChoice = WinTrustUnionChoice.WTD_CHOICE_BLOB;
wtData.pChoice = &wbi;

return WinTrustMethods.WinVerifyTrust(IntPtr.Zero,
return WinTrustMethods.WinVerifyTrust(
IntPtr.Zero,
ref WINTRUST_ACTION_GENERIC_VERIFY_V2,
ref wtData);
}
Expand Down
19 changes: 15 additions & 4 deletions src/System.Management.Automation/security/SecurityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,16 +534,16 @@ private static Signature GetSignatureWithEncodingRetry(string path, ExternalScri

// try harder to validate the signature by being explicit about encoding
// and providing the script contents
string verificationContents = Encoding.Unicode.GetString(script.OriginalEncoding.GetPreamble()) + script.ScriptContents;
signature = SignatureHelper.GetSignature(path, verificationContents);
byte[] bytesWithBom = GetContentBytesWithBom(script.OriginalEncoding, script.ScriptContents);
signature = SignatureHelper.GetSignature(path, bytesWithBom);

// A last ditch effort -
// If the file was originally ASCII or UTF8, the SIP may have added the Unicode BOM
if (signature.Status != SignatureStatus.Valid
&& script.OriginalEncoding != Encoding.Unicode)
{
verificationContents = Encoding.Unicode.GetString(Encoding.Unicode.GetPreamble()) + script.ScriptContents;
Signature fallbackSignature = SignatureHelper.GetSignature(path, verificationContents);
bytesWithBom = GetContentBytesWithBom(Encoding.Unicode, script.ScriptContents);
Signature fallbackSignature = SignatureHelper.GetSignature(path, bytesWithBom);

if (fallbackSignature.Status == SignatureStatus.Valid)
signature = fallbackSignature;
Expand All @@ -552,6 +552,17 @@ private static Signature GetSignatureWithEncodingRetry(string path, ExternalScri
return signature;
}

private static byte[] GetContentBytesWithBom(Encoding encoding, string scriptContent)
{
ReadOnlySpan<byte> bomBytes = encoding.Preamble;
byte[] contentBytes = encoding.GetBytes(scriptContent);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unsure if this code path warrants it, but you could ArrayPool<byte>.Shared.Rent(encoding.GetMaxCharCount(scriptContent)) here to avoid some potentially LOH allocations here

Copy link
Member

Choose a reason for hiding this comment

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

It's been using System.Text.Encoding.Unicode.GetBytes(fileContent); in GetWinTrustData method, so I guess the LOH allocation has not become a concern, but yes, it's a potential perf issue.

byte[] bytesWithBom = new byte[bomBytes.Length + contentBytes.Length];

bomBytes.CopyTo(bytesWithBom);
contentBytes.CopyTo(bytesWithBom, index: bomBytes.Length);
return bytesWithBom;
}

#endregion signing check

/// <summary>
Expand Down
148 changes: 148 additions & 0 deletions test/powershell/engine/Security/FileSignature.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,151 @@ Describe "Windows platform file signatures" -Tags 'Feature' {
$signature.SignatureType | Should -BeExactly 'Catalog'
}
}

Describe "Windows file content signatures" -Tags @('Feature', 'RequireAdminOnWindows') {
$PSDefaultParameterValues = @{ "It:Skip" = (-not $IsWindows) }

BeforeAll {
$session = New-PSSession -UseWindowsPowerShell
try {
# New-SelfSignedCertificate runs in implicit remoting so do all the
# setup work over there
$caRootThumbprint, $signingThumbprint = Invoke-Command -Session $session -ScriptBlock {
$testPrefix = 'SelfSignedTest'

$enhancedKeyUsage = [Security.Cryptography.OidCollection]::new()
$null = $enhancedKeyUsage.Add('1.3.6.1.5.5.7.3.3') # Code Signing

$caParams = @{
Extension = @(
[Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true),
[Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new('KeyCertSign', $false),
[Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension ]::new($enhancedKeyUsage, $false)
)
CertStoreLocation = 'Cert:\CurrentUser\My'
NotAfter = (Get-Date).AddDays(1)
Type = 'Custom'
}
$caRoot = PKI\New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-CA"

$rootStore = Get-Item -Path Cert:\LocalMachine\Root
$rootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
try {
$rootStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($caRoot.RawData))
} finally {
$rootStore.Close()
}

$certParams = @{
CertStoreLocation = 'Cert:\CurrentUser\My'
KeyUsage = 'DigitalSignature'
TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
Type = 'Custom'
}
$certificate = PKI\New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Signed" -Signer $caRoot

$publisherStore = Get-Item -Path Cert:\LocalMachine\TrustedPublisher
$publisherStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
try {
$publisherStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certificate.RawData))
} finally {
$publisherStore.Close()
}

$caRoot | Remove-Item

$caRoot.Thumbprint, $certificate.Thumbprint
}
} finally {
$session | Remove-PSSession
}

$certificate = Get-Item -Path Cert:\CurrentUser\My\$signingThumbprint
}

AfterAll {
Remove-Item -Path Cert:\LocalMachine\Root\$caRootThumbprint -Force
Remove-Item -Path Cert:\LocalMachine\TrustedPublisher\$signingThumbprint -Force
Remove-Item -Path Cert:\CurrentUser\My\$signingThumbprint -Force
}

It "Validates signature using path on even char count with Encoding <Encoding>" -TestCases @(
@{ Encoding = 'ASCII' }
@{ Encoding = 'Unicode' }
@{ Encoding = 'UTF8BOM' }
@{ Encoding = 'UTF8NoBOM' }
) {
param ($Encoding)

Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World"' -Encoding $Encoding

$scriptPath = Join-Path $TestDrive test.ps1
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
$status.Status | Should -Be 'Valid'

$actual = Get-AuthenticodeSignature -FilePath $scriptPath
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
$actual.Status | Should -Be 'Valid'
}

It "Validates signature using path on odd char count with Encoding <Encoding>" -TestCases @(
@{ Encoding = 'ASCII' }
@{ Encoding = 'Unicode' }
@{ Encoding = 'UTF8BOM' }
@{ Encoding = 'UTF8NoBOM' }
) {
param ($Encoding)

Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World!"' -Encoding $Encoding

$scriptPath = Join-Path $TestDrive test.ps1
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
$status.Status | Should -Be 'Valid'

$actual = Get-AuthenticodeSignature -FilePath $scriptPath
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
$actual.Status | Should -Be 'Valid'
}

It "Validates signature using content on even char count with Encoding <Encoding>" -TestCases @(
@{ Encoding = 'ASCII' }
@{ Encoding = 'Unicode' }
@{ Encoding = 'UTF8BOM' }
@{ Encoding = 'UTF8NoBOM' }
) {
param ($Encoding)

Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World"' -Encoding $Encoding

$scriptPath = Join-Path $TestDrive test.ps1
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
$status.Status | Should -Be 'Valid'

$fileBytes = Get-Content -Path testdrive:\test.ps1 -AsByteStream

$actual = Get-AuthenticodeSignature -Content $fileBytes -SourcePathOrExtension .ps1
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
$actual.Status | Should -Be 'Valid'
}

It "Validates signature using content on odd char count with Encoding <Encoding>" -TestCases @(
@{ Encoding = 'ASCII' }
@{ Encoding = 'Unicode' }
@{ Encoding = 'UTF8BOM' }
@{ Encoding = 'UTF8NoBOM' }
) {
param ($Encoding)

Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World!"' -Encoding $Encoding

$scriptPath = Join-Path $TestDrive test.ps1
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
$status.Status | Should -Be 'Valid'

$fileBytes = Get-Content -Path testdrive:\test.ps1 -AsByteStream

$actual = Get-AuthenticodeSignature -Content $fileBytes -SourcePathOrExtension .ps1
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
$actual.Status | Should -Be 'Valid'
}
}