Skip to content

Commit 81e645c

Browse files
committed
chore: tests for authenticode and version checks
1 parent ee33e47 commit 81e645c

File tree

12 files changed

+224
-20
lines changed

12 files changed

+224
-20
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Coder Desktop for Windows
2+
3+
This repo contains the C# source code for Coder Desktop for Windows. You can
4+
download the latest version from the GitHub releases.
5+
6+
### Contributing
7+
8+
You will need:
9+
10+
- Visual Studio 2022
11+
- .NET desktop development
12+
- WinUI application development
13+
- Windows 10 SDK (10.0.19041.0)
14+
- Wix Toolset 5.0.2 (if building the installer)
15+
16+
It's also recommended to use JetBrains Rider (or VS + ReSharper) for a better
17+
experience.
18+
19+
### License
20+
21+
The Coder Desktop for Windows source is licensed under the GNU Affero General
22+
Public License v3.0 (AGPL-3.0).
23+
24+
Some vendored files in this repo are licensed separately. The license for these
25+
files can be found in the same directory as the files.
26+
27+
The binary distributions of Coder Desktop for Windows have some additional
28+
license disclaimers that can be found in
29+
[scripts/files/License.txt](scripts/files/License.txt) or during installation.

Tests.Vpn.Service/DownloaderTest.cs

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,29 @@ public class AuthenticodeDownloadValidatorTest
2727
[CancelAfter(30_000)]
2828
public void Unsigned(CancellationToken ct)
2929
{
30-
// TODO: this
30+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello.exe");
31+
var ex = Assert.ThrowsAsync<Exception>(() =>
32+
AuthenticodeDownloadValidator.Coder.ValidateAsync(testBinaryPath, ct));
33+
Assert.That(ex.Message, Does.Contain("File is not signed and trusted with an Authenticode signature: State=Unsigned, StateReason=None"));
3134
}
3235

3336
[Test(Description = "Test an untrusted binary")]
3437
[CancelAfter(30_000)]
3538
public void Untrusted(CancellationToken ct)
3639
{
37-
// TODO: this
40+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello-self-signed.exe");
41+
var ex = Assert.ThrowsAsync<Exception>(() =>
42+
AuthenticodeDownloadValidator.Coder.ValidateAsync(testBinaryPath, ct));
43+
Assert.That(ex.Message, Does.Contain("File is not signed and trusted with an Authenticode signature: State=Unsigned, StateReason=UntrustedRoot"));
3844
}
3945

4046
[Test(Description = "Test an binary with a detached signature (catalog file)")]
4147
[CancelAfter(30_000)]
4248
public void DifferentCertTrusted(CancellationToken ct)
4349
{
44-
// notepad.exe uses a catalog file for its signature.
50+
// rundll32.exe uses a catalog file for its signature.
4551
var ex = Assert.ThrowsAsync<Exception>(() =>
46-
AuthenticodeDownloadValidator.Coder.ValidateAsync(@"C:\Windows\System32\notepad.exe", ct));
52+
AuthenticodeDownloadValidator.Coder.ValidateAsync(@"C:\Windows\System32\rundll32.exe", ct));
4753
Assert.That(ex.Message,
4854
Does.Contain("File is not signed with an embedded Authenticode signature: Kind=Catalog"));
4955
}
@@ -52,15 +58,19 @@ public void DifferentCertTrusted(CancellationToken ct)
5258
[CancelAfter(30_000)]
5359
public void DifferentCertUntrusted(CancellationToken ct)
5460
{
55-
// TODO: this
61+
// dotnet.exe is signed by .NET. During tests we can be pretty sure
62+
// this is installed.
63+
var ex = Assert.ThrowsAsync<Exception>(() =>
64+
AuthenticodeDownloadValidator.Coder.ValidateAsync(@"C:\Program Files\dotnet\dotnet.exe", ct));
65+
Assert.That(ex.Message, Does.Contain("File is signed by an unexpected certificate: ExpectedName='Coder Technologies Inc.', ActualName='.NET"));
5666
}
5767

5868
[Test(Description = "Test a binary signed by Coder's certificate")]
5969
[CancelAfter(30_000)]
6070
public async Task CoderSigned(CancellationToken ct)
6171
{
62-
// TODO: this
63-
await Task.CompletedTask;
72+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello-versioned-signed.exe");
73+
await AuthenticodeDownloadValidator.Coder.ValidateAsync(testBinaryPath, ct);
6474
}
6575
}
6676

@@ -71,22 +81,57 @@ public class AssemblyVersionDownloadValidatorTest
7181
[CancelAfter(30_000)]
7282
public void NoVersion(CancellationToken ct)
7383
{
74-
// TODO: this
84+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello.exe");
85+
var ex = Assert.ThrowsAsync<Exception>(() =>
86+
new AssemblyVersionDownloadValidator(1, 2, 3, 4).ValidateAsync(testBinaryPath, ct));
87+
Assert.That(ex.Message, Does.Contain("File ProductVersion is empty or null"));
7588
}
7689

77-
[Test(Description = "Version mismatch")]
90+
[Test(Description = "Invalid version on binary")]
7891
[CancelAfter(30_000)]
79-
public void VersionMismatch(CancellationToken ct)
92+
public void InvalidVersion(CancellationToken ct)
8093
{
81-
// TODO: this
94+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello-invalid-version.exe");
95+
var ex = Assert.ThrowsAsync<Exception>(() =>
96+
new AssemblyVersionDownloadValidator(1, 2, 3, 4).ValidateAsync(testBinaryPath, ct));
97+
Assert.That(ex.Message, Does.Contain("File ProductVersion '1-2-3-4' is not a valid version string"));
98+
}
99+
100+
[Test(Description = "Version mismatch with full version check")]
101+
[CancelAfter(30_000)]
102+
public void VersionMismatchFull(CancellationToken ct)
103+
{
104+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello-versioned-signed.exe");
105+
106+
// Try changing each version component one at a time
107+
var expectedVersions = new[] { 1, 2, 3, 4 };
108+
for (var i = 0; i < 4; i++)
109+
{
110+
var testVersions = (int[])expectedVersions.Clone();
111+
testVersions[i]++; // Increment this component to make it wrong
112+
113+
var ex = Assert.ThrowsAsync<Exception>(() =>
114+
new AssemblyVersionDownloadValidator(
115+
testVersions[0], testVersions[1], testVersions[2], testVersions[3]
116+
).ValidateAsync(testBinaryPath, ct));
117+
118+
Assert.That(ex.Message, Does.Contain(
119+
$"File ProductVersion does not match expected version: Actual='1.2.3.4', Expected='{string.Join(".", testVersions)}'"));
120+
}
82121
}
83122

84-
[Test(Description = "Version match")]
123+
[Test(Description = "Version match with and without partial version check")]
85124
[CancelAfter(30_000)]
86125
public async Task VersionMatch(CancellationToken ct)
87126
{
88-
// TODO: this
89-
await Task.CompletedTask;
127+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "hello-versioned-signed.exe");
128+
129+
// Test with just major.minor
130+
await new AssemblyVersionDownloadValidator(1, 2).ValidateAsync(testBinaryPath, ct);
131+
// Test with major.minor.patch
132+
await new AssemblyVersionDownloadValidator(1, 2, 3).ValidateAsync(testBinaryPath, ct);
133+
// Test with major.minor.patch.build
134+
await new AssemblyVersionDownloadValidator(1, 2, 3, 4).ValidateAsync(testBinaryPath, ct);
90135
}
91136
}
92137

Tests.Vpn.Service/Tests.Vpn.Service.csproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@
1212
<IsTestProject>true</IsTestProject>
1313
</PropertyGroup>
1414

15+
<ItemGroup>
16+
<None Remove="testdata\hello.go" />
17+
<None Remove="testdata\winres.json" />
18+
<None Update="testdata\hello.exe">
19+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
20+
</None>
21+
<None Update="testdata\hello-invalid-version.exe">
22+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
23+
</None>
24+
<None Update="testdata\hello-self-signed.exe">
25+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
26+
</None>
27+
<None Update="testdata\hello-versioned-signed.exe">
28+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
29+
</None>
30+
</ItemGroup>
31+
1532
<ItemGroup>
1633
<PackageReference Include="coverlet.collector" Version="6.0.4">
1734
<PrivateAssets>all</PrivateAssets>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.go
2+
*.pfx
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
$errorActionPreference = "Stop"
2+
3+
Set-Location $PSScriptRoot
4+
5+
# If hello.go does not exist, write it. We don't check it into the repo to avoid
6+
# GitHub showing that the repo contains Go code.
7+
if (-not (Test-Path "hello.go")) {
8+
$helloGo = @"
9+
package main
10+
11+
func main() {
12+
println("Hello, World!")
13+
}
14+
"@
15+
Set-Content -Path "hello.go" -Value $helloGo
16+
}
17+
18+
& go.exe build -ldflags '-w -s' -o hello.exe hello.go
19+
if ($LASTEXITCODE -ne 0) { throw "Failed to build hello.exe" }
20+
21+
# hello-invalid-version.exe is used for testing versioned binaries with an
22+
# invalid version.
23+
Copy-Item hello.exe hello-invalid-version.exe
24+
& go-winres.exe patch --in winres.json --delete --no-backup --product-version 1-2-3-4 --file-version 1-2-3-4 hello-invalid-version.exe
25+
if ($LASTEXITCODE -ne 0) { throw "Failed to patch hello-invalid-version.exe with go-winres" }
26+
27+
# hello-self-signed.exe is used for testing untrusted binaries.
28+
Copy-Item hello.exe hello-self-signed.exe
29+
$helloSelfSignedPath = (Get-Item hello-self-signed.exe).FullName
30+
31+
# Create a self signed certificate for signing and then delete it.
32+
$certStoreLocation = "Cert:\CurrentUser\My"
33+
$password = "password"
34+
$cert = New-SelfSignedCertificate `
35+
-CertStoreLocation $certStoreLocation `
36+
-DnsName coder.com `
37+
-Subject "CN=coder-desktop-windows-self-signed-cert" `
38+
-Type CodeSigningCert `
39+
-KeyUsage DigitalSignature `
40+
-NotAfter (Get-Date).AddDays(3650)
41+
$pfxPath = Join-Path $PSScriptRoot "cert.pfx"
42+
try {
43+
$securePassword = ConvertTo-SecureString -String $password -Force -AsPlainText
44+
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $securePassword
45+
46+
# Sign hello-self-signed.exe with the self signed certificate
47+
& "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe" sign /debug /f $pfxPath /p $password /tr "http://timestamp.digicert.com" /td sha256 /fd sha256 $helloSelfSignedPath
48+
if ($LASTEXITCODE -ne 0) { throw "Failed to sign hello-self-signed.exe with signtool" }
49+
} finally {
50+
if ($cert.Thumbprint) {
51+
Remove-Item -Path (Join-Path $certStoreLocation $cert.Thumbprint) -Force
52+
}
53+
if (Test-Path $pfxPath) {
54+
Remove-Item -Path $pfxPath -Force
55+
}
56+
}
57+
58+
# hello-versioned-signed.exe is used for testing versioned binariess and
59+
# binaries signed by a real EV certificate.
60+
Copy-Item hello.exe hello-versioned-signed.exe
61+
62+
& go-winres.exe patch --in winres.json --delete --no-backup --product-version 1.2.3.4 --file-version 1.2.3.4 hello-versioned-signed.exe
63+
if ($LASTEXITCODE -ne 0) { throw "Failed to patch hello-versioned-signed.exe with go-winres" }
64+
65+
# Then sign hello-versioned-signed.exe with the same EV cert as our real
66+
# binaries. Since this is a bit more complicated and requires some extra
67+
# permissions, we don't do this in the build script.
68+
Write-Host "Don't forget to sign hello-versioned-signed.exe with the EV cert!"
1010 KB
Binary file not shown.
1020 KB
Binary file not shown.
1020 KB
Binary file not shown.
1010 KB
Binary file not shown.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"RT_MANIFEST": {
3+
"#1": {
4+
"0409": {
5+
"identity": {},
6+
"description": "",
7+
"minimum-os": "win7",
8+
"execution-level": "",
9+
"ui-access": false,
10+
"auto-elevate": false,
11+
"dpi-awareness": "system",
12+
"disable-theming": false,
13+
"disable-window-filtering": false,
14+
"high-resolution-scrolling-aware": false,
15+
"ultra-high-resolution-scrolling-aware": false,
16+
"long-path-aware": false,
17+
"printer-driver-isolation": false,
18+
"gdi-scaling": false,
19+
"segment-heap": false,
20+
"use-common-controls-v6": false
21+
}
22+
}
23+
},
24+
"RT_VERSION": {
25+
"#1": {
26+
"0409": {
27+
"fixed": {
28+
"file_version": "1.2.3.4",
29+
"product_version": "1.2.3.4"
30+
},
31+
"info": {
32+
"0409": {
33+
"FileDescription": "Coder",
34+
"FileVersion": "1.2.3.4",
35+
"LegalCopyright": "Copyright 2025 Coder Technologies Inc.",
36+
"OriginalFilename": "coder.exe",
37+
"ProductName": "Coder",
38+
"ProductVersion": "1.2.3.4"
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)