Skip to content

Commit 93e9c63

Browse files
authored
Fix Get-AuthenticodeSignature -Content to not roundtrip the bytes to a Unicode string and then back to bytes (PowerShell#18774)
1 parent cf6fc53 commit 93e9c63

File tree

4 files changed

+179
-17
lines changed

4 files changed

+179
-17
lines changed

src/Microsoft.PowerShell.Security/security/SignatureCommands.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ protected override Signature PerformAction(string filePath)
294294
/// </returns>
295295
protected override Signature PerformAction(string sourcePathOrExtension, byte[] content)
296296
{
297-
return SignatureHelper.GetSignature(sourcePathOrExtension, System.Text.Encoding.Unicode.GetString(content));
297+
return SignatureHelper.GetSignature(sourcePathOrExtension, content);
298298
}
299299
}
300300

src/System.Management.Automation/security/Authenticode.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ internal static Signature SignFile(SigningOption option,
275275
/// <exception cref="System.IO.FileNotFoundException">
276276
/// Thrown if the file specified by argument fileName is not found.
277277
/// </exception>
278-
internal static Signature GetSignature(string fileName, string fileContent)
278+
internal static Signature GetSignature(string fileName, byte[] fileContent)
279279
{
280280
Signature signature = null;
281281

@@ -398,7 +398,7 @@ private static uint GetErrorFromSignatureState(SignatureState signatureState)
398398
}
399399
#endif
400400

401-
private static Signature GetSignatureFromWinVerifyTrust(string fileName, string fileContent)
401+
private static Signature GetSignatureFromWinVerifyTrust(string fileName, byte[] fileContent)
402402
{
403403
Signature signature = null;
404404

@@ -409,6 +409,7 @@ private static Signature GetSignatureFromWinVerifyTrust(string fileName, string
409409
{
410410
Utils.CheckArgForNullOrEmpty(fileName, "fileName");
411411
SecuritySupport.CheckIfFileExists(fileName);
412+
412413
// SecurityUtils.CheckIfFileSmallerThan4Bytes(fileName);
413414
}
414415

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

426427
wtd.dwStateAction = WinTrustAction.WTD_STATEACTION_CLOSE;
427-
error = WinTrustMethods.WinVerifyTrust(IntPtr.Zero,
428+
error = WinTrustMethods.WinVerifyTrust(
429+
IntPtr.Zero,
428430
ref WINTRUST_ACTION_GENERIC_VERIFY_V2,
429431
ref wtd);
430432

@@ -441,8 +443,10 @@ private static Signature GetSignatureFromWinVerifyTrust(string fileName, string
441443
return signature;
442444
}
443445

444-
private static uint GetWinTrustData(string fileName, string fileContent,
445-
out WinTrustMethods.WINTRUST_DATA wtData)
446+
private static uint GetWinTrustData(
447+
string fileName,
448+
byte[] fileContent,
449+
out WinTrustMethods.WINTRUST_DATA wtData)
446450
{
447451
wtData = new()
448452
{
@@ -451,9 +455,6 @@ private static uint GetWinTrustData(string fileName, string fileContent,
451455
dwStateAction = WinTrustAction.WTD_STATEACTION_VERIFY,
452456
};
453457

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

471-
return WinTrustMethods.WinVerifyTrust(IntPtr.Zero,
472+
return WinTrustMethods.WinVerifyTrust(
473+
IntPtr.Zero,
472474
ref WINTRUST_ACTION_GENERIC_VERIFY_V2,
473475
ref wtData);
474476
}
475477

476-
fixed (byte* contentPtr = contentBytes)
478+
fixed (byte* contentPtr = fileContent)
477479
{
478480
Guid pwshSIP = new("603BCC1F-4B59-4E08-B724-D2C6297EF351");
479481
WinTrustMethods.WINTRUST_BLOB_INFO wbi = new()
480482
{
481483
cbStruct = (uint)Marshal.SizeOf<WinTrustMethods.WINTRUST_BLOB_INFO>(),
482484
gSubject = pwshSIP,
483485
pcwszDisplayName = fileNamePtr,
484-
cbMemObject = (uint)contentBytes.Length,
486+
cbMemObject = (uint)fileContent.Length,
485487
pbMemObject = contentPtr,
486488
};
487489
wtData.dwUnionChoice = WinTrustUnionChoice.WTD_CHOICE_BLOB;
488490
wtData.pChoice = &wbi;
489491

490-
return WinTrustMethods.WinVerifyTrust(IntPtr.Zero,
492+
return WinTrustMethods.WinVerifyTrust(
493+
IntPtr.Zero,
491494
ref WINTRUST_ACTION_GENERIC_VERIFY_V2,
492495
ref wtData);
493496
}

src/System.Management.Automation/security/SecurityManager.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,16 +542,16 @@ private static Signature GetSignatureWithEncodingRetry(string path, ExternalScri
542542

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

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

556556
if (fallbackSignature.Status == SignatureStatus.Valid)
557557
signature = fallbackSignature;
@@ -560,6 +560,17 @@ private static Signature GetSignatureWithEncodingRetry(string path, ExternalScri
560560
return signature;
561561
}
562562

563+
private static byte[] GetContentBytesWithBom(Encoding encoding, string scriptContent)
564+
{
565+
ReadOnlySpan<byte> bomBytes = encoding.Preamble;
566+
byte[] contentBytes = encoding.GetBytes(scriptContent);
567+
byte[] bytesWithBom = new byte[bomBytes.Length + contentBytes.Length];
568+
569+
bomBytes.CopyTo(bytesWithBom);
570+
contentBytes.CopyTo(bytesWithBom, index: bomBytes.Length);
571+
return bytesWithBom;
572+
}
573+
563574
#endregion signing check
564575

565576
/// <summary>

test/powershell/engine/Security/FileSignature.Tests.ps1

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,151 @@ Describe "Windows platform file signatures" -Tags 'Feature' {
2020
$signature.SignatureType | Should -BeExactly 'Catalog'
2121
}
2222
}
23+
24+
Describe "Windows file content signatures" -Tags @('Feature', 'RequireAdminOnWindows') {
25+
$PSDefaultParameterValues = @{ "It:Skip" = (-not $IsWindows) }
26+
27+
BeforeAll {
28+
$session = New-PSSession -UseWindowsPowerShell
29+
try {
30+
# New-SelfSignedCertificate runs in implicit remoting so do all the
31+
# setup work over there
32+
$caRootThumbprint, $signingThumbprint = Invoke-Command -Session $session -ScriptBlock {
33+
$testPrefix = 'SelfSignedTest'
34+
35+
$enhancedKeyUsage = [Security.Cryptography.OidCollection]::new()
36+
$null = $enhancedKeyUsage.Add('1.3.6.1.5.5.7.3.3') # Code Signing
37+
38+
$caParams = @{
39+
Extension = @(
40+
[Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true),
41+
[Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new('KeyCertSign', $false),
42+
[Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension ]::new($enhancedKeyUsage, $false)
43+
)
44+
CertStoreLocation = 'Cert:\CurrentUser\My'
45+
NotAfter = (Get-Date).AddDays(1)
46+
Type = 'Custom'
47+
}
48+
$caRoot = PKI\New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-CA"
49+
50+
$rootStore = Get-Item -Path Cert:\LocalMachine\Root
51+
$rootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
52+
try {
53+
$rootStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($caRoot.RawData))
54+
} finally {
55+
$rootStore.Close()
56+
}
57+
58+
$certParams = @{
59+
CertStoreLocation = 'Cert:\CurrentUser\My'
60+
KeyUsage = 'DigitalSignature'
61+
TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
62+
Type = 'Custom'
63+
}
64+
$certificate = PKI\New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Signed" -Signer $caRoot
65+
66+
$publisherStore = Get-Item -Path Cert:\LocalMachine\TrustedPublisher
67+
$publisherStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
68+
try {
69+
$publisherStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certificate.RawData))
70+
} finally {
71+
$publisherStore.Close()
72+
}
73+
74+
$caRoot | Remove-Item
75+
76+
$caRoot.Thumbprint, $certificate.Thumbprint
77+
}
78+
} finally {
79+
$session | Remove-PSSession
80+
}
81+
82+
$certificate = Get-Item -Path Cert:\CurrentUser\My\$signingThumbprint
83+
}
84+
85+
AfterAll {
86+
Remove-Item -Path Cert:\LocalMachine\Root\$caRootThumbprint -Force
87+
Remove-Item -Path Cert:\LocalMachine\TrustedPublisher\$signingThumbprint -Force
88+
Remove-Item -Path Cert:\CurrentUser\My\$signingThumbprint -Force
89+
}
90+
91+
It "Validates signature using path on even char count with Encoding <Encoding>" -TestCases @(
92+
@{ Encoding = 'ASCII' }
93+
@{ Encoding = 'Unicode' }
94+
@{ Encoding = 'UTF8BOM' }
95+
@{ Encoding = 'UTF8NoBOM' }
96+
) {
97+
param ($Encoding)
98+
99+
Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World"' -Encoding $Encoding
100+
101+
$scriptPath = Join-Path $TestDrive test.ps1
102+
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
103+
$status.Status | Should -Be 'Valid'
104+
105+
$actual = Get-AuthenticodeSignature -FilePath $scriptPath
106+
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
107+
$actual.Status | Should -Be 'Valid'
108+
}
109+
110+
It "Validates signature using path on odd char count with Encoding <Encoding>" -TestCases @(
111+
@{ Encoding = 'ASCII' }
112+
@{ Encoding = 'Unicode' }
113+
@{ Encoding = 'UTF8BOM' }
114+
@{ Encoding = 'UTF8NoBOM' }
115+
) {
116+
param ($Encoding)
117+
118+
Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World!"' -Encoding $Encoding
119+
120+
$scriptPath = Join-Path $TestDrive test.ps1
121+
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
122+
$status.Status | Should -Be 'Valid'
123+
124+
$actual = Get-AuthenticodeSignature -FilePath $scriptPath
125+
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
126+
$actual.Status | Should -Be 'Valid'
127+
}
128+
129+
It "Validates signature using content on even char count with Encoding <Encoding>" -TestCases @(
130+
@{ Encoding = 'ASCII' }
131+
@{ Encoding = 'Unicode' }
132+
@{ Encoding = 'UTF8BOM' }
133+
@{ Encoding = 'UTF8NoBOM' }
134+
) {
135+
param ($Encoding)
136+
137+
Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World"' -Encoding $Encoding
138+
139+
$scriptPath = Join-Path $TestDrive test.ps1
140+
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
141+
$status.Status | Should -Be 'Valid'
142+
143+
$fileBytes = Get-Content -Path testdrive:\test.ps1 -AsByteStream
144+
145+
$actual = Get-AuthenticodeSignature -Content $fileBytes -SourcePathOrExtension .ps1
146+
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
147+
$actual.Status | Should -Be 'Valid'
148+
}
149+
150+
It "Validates signature using content on odd char count with Encoding <Encoding>" -TestCases @(
151+
@{ Encoding = 'ASCII' }
152+
@{ Encoding = 'Unicode' }
153+
@{ Encoding = 'UTF8BOM' }
154+
@{ Encoding = 'UTF8NoBOM' }
155+
) {
156+
param ($Encoding)
157+
158+
Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Hello World!"' -Encoding $Encoding
159+
160+
$scriptPath = Join-Path $TestDrive test.ps1
161+
$status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate
162+
$status.Status | Should -Be 'Valid'
163+
164+
$fileBytes = Get-Content -Path testdrive:\test.ps1 -AsByteStream
165+
166+
$actual = Get-AuthenticodeSignature -Content $fileBytes -SourcePathOrExtension .ps1
167+
$actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint
168+
$actual.Status | Should -Be 'Valid'
169+
}
170+
}

0 commit comments

Comments
 (0)