Skip to content

Commit ecf8260

Browse files
committed
feat: EV validation
1 parent bc09aa2 commit ecf8260

14 files changed

+291
-15
lines changed

Installer/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,9 @@ private static int BuildMsiPackage(MsiOptions opts)
250250
programFiles64Folder.AddDir(installDir);
251251
project.AddDir(programFiles64Folder);
252252

253-
// Add registry values that are consumed by the manager.
253+
// Add registry values that are consumed by the manager. Note that these
254+
// should not be changed. See Vpn.Service/Program.cs and
255+
// Vpn.Service/ManagerConfig.cs for more details.
254256
project.AddRegValues(
255257
new RegValue(RegistryHive, RegistryKey, "Manager:ServiceRpcPipeName", "Coder.Desktop.Vpn"),
256258
new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryPath",

Tests.Vpn.Service/DownloaderTest.cs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Reflection;
12
using System.Security.Cryptography;
3+
using System.Security.Cryptography.X509Certificates;
24
using System.Text;
35
using Coder.Desktop.Vpn.Service;
46
using Microsoft.Extensions.Logging.Abstractions;
@@ -59,17 +61,30 @@ public void DifferentCertTrusted(CancellationToken ct)
5961
Does.Contain("File is not signed with an embedded Authenticode signature: Kind=Catalog"));
6062
}
6163

62-
[Test(Description = "Test a binary signed by a different certificate")]
64+
[Test(Description = "Test a binary signed by a non-EV certificate")]
6365
[CancelAfter(30_000)]
64-
public void DifferentCertUntrusted(CancellationToken ct)
66+
public void NonEvCert(CancellationToken ct)
6567
{
6668
// dotnet.exe is signed by .NET. During tests we can be pretty sure
6769
// this is installed.
6870
var ex = Assert.ThrowsAsync<Exception>(() =>
6971
AuthenticodeDownloadValidator.Coder.ValidateAsync(@"C:\Program Files\dotnet\dotnet.exe", ct));
7072
Assert.That(ex.Message,
7173
Does.Contain(
72-
"File is signed by an unexpected certificate: ExpectedName='Coder Technologies Inc.', ActualName='.NET"));
74+
"File is not signed with an Extended Validation Code Signing certificate"));
75+
}
76+
77+
[Test(Description = "Test a binary signed by an EV certificate with a different name")]
78+
[CancelAfter(30_000)]
79+
public void EvDifferentCertName(CancellationToken ct)
80+
{
81+
var testBinaryPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata",
82+
"hello-versioned-signed.exe");
83+
var ex = Assert.ThrowsAsync<Exception>(() =>
84+
new AuthenticodeDownloadValidator("Acme Corporation").ValidateAsync(testBinaryPath, ct));
85+
Assert.That(ex.Message,
86+
Does.Contain(
87+
"File is signed by an unexpected certificate: ExpectedName='Acme Corporation', ActualName='Coder Technologies Inc.'"));
7388
}
7489

7590
[Test(Description = "Test a binary signed by Coder's certificate")]
@@ -80,6 +95,37 @@ public async Task CoderSigned(CancellationToken ct)
8095
"hello-versioned-signed.exe");
8196
await AuthenticodeDownloadValidator.Coder.ValidateAsync(testBinaryPath, ct);
8297
}
98+
99+
[Test(Description = "Test if the EV check works")]
100+
public void IsEvCert()
101+
{
102+
// To avoid potential API misuse the function is private.
103+
var method = typeof(AuthenticodeDownloadValidator).GetMethod("IsExtendedValidationCertificate",
104+
BindingFlags.NonPublic | BindingFlags.Static);
105+
Assert.That(method, Is.Not.Null, "Could not find IsExtendedValidationCertificate method");
106+
107+
// Call it with various certificates.
108+
var certs = new List<(string, bool)>
109+
{
110+
// EV:
111+
(Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "coder-ev.crt"), true),
112+
(Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "google-llc-ev.crt"), true),
113+
(Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "self-signed-ev.crt"), true),
114+
// Not EV:
115+
(Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "mozilla-corporation.crt"), false),
116+
(Path.Combine(TestContext.CurrentContext.TestDirectory, "testdata", "self-signed.crt"), false),
117+
};
118+
119+
foreach (var (certPath, isEv) in certs)
120+
{
121+
var x509Cert = new X509Certificate2(certPath);
122+
var result = (bool?)method!.Invoke(null, [x509Cert]);
123+
Assert.That(result, Is.Not.Null,
124+
$"IsExtendedValidationCertificate returned null for {Path.GetFileName(certPath)}");
125+
Assert.That(result, Is.EqualTo(isEv),
126+
$"IsExtendedValidationCertificate returned wrong result for {Path.GetFileName(certPath)}");
127+
}
128+
}
83129
}
84130

85131
[TestFixture]

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
</PropertyGroup>
1414

1515
<ItemGroup>
16-
<None Remove="testdata\hello.go" />
17-
<None Remove="testdata\winres.json" />
1816
<None Update="testdata\hello.exe">
1917
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2018
</None>
@@ -27,6 +25,21 @@
2725
<None Update="testdata\hello-versioned-signed.exe">
2826
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2927
</None>
28+
<None Update="testdata\coder-ev.crt">
29+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
30+
</None>
31+
<None Update="testdata\google-llc-ev.crt">
32+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
33+
</None>
34+
<None Update="testdata\mozilla-corporation.crt">
35+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
36+
</None>
37+
<None Update="testdata\self-signed.crt">
38+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
39+
</None>
40+
<None Update="testdata\self-signed-ev.crt">
41+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
42+
</None>
3043
</ItemGroup>
3144

3245
<ItemGroup>

Tests.Vpn.Service/testdata/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Tests.Vpn.Service testdata
2+
3+
### Executables
4+
5+
`Build-Assets.ps1` creates `hello.exe` and derivatives. You need `go`,
6+
`go-winres` and Windows 10 SDK 10.0.19041.0 installed to run this.
7+
8+
You must sign `hello-versioned-signed.exe` yourself with the Coder EV cert after
9+
the script completes.
10+
11+
These files are checked into the repo so they shouldn't need to be built again.
12+
13+
### Certificates
14+
15+
- `coder-ev.crt` is the Extended Validation Code Signing certificate used by
16+
Coder, extracted from a signed release binary on 2025-03-07
17+
- `google-llc-ev.crt` is the Extended Validation Code Signing certificate used
18+
by Google Chrome, extracted from an official binary on 2025-03-07
19+
- `mozilla-corporation.crt` is a regular Code Signing certificate used by
20+
Mozilla Firefox, extracted from an official binary on 2025-03-07
21+
- `self-signed-ev.crt` was generated with `gen-certs.sh` using Linux OpenSSL
22+
- `self-signed.crt` was generated with `gen-certs.sh` using Linux OpenSSL
23+
24+
You can extract a certificate from an executable with the following PowerShell
25+
one-liner:
26+
27+
```powershell
28+
Get-AuthenticodeSignature binary.exe | Select-Object -ExpandProperty SignerCertificate | Export-Certificate -Type CERT -FilePath output.crt
29+
```
1.95 KB
Binary file not shown.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Generate a regular code signing certificate without the EV policy OID.
5+
openssl req \
6+
-x509 \
7+
-newkey rsa:2048 \
8+
-keyout /dev/null \
9+
-out self-signed.crt \
10+
-days 3650 \
11+
-nodes \
12+
-subj "/CN=Coder Self Signed" \
13+
-addext "keyUsage=digitalSignature" \
14+
-addext "extendedKeyUsage=codeSigning"
15+
16+
# Generate an EV code signing certificate by adding the EV policy OID. We add
17+
# a different OID before the EV OID to ensure the validator can handle multiple
18+
# policies.
19+
config="
20+
[req]
21+
distinguished_name = req_distinguished_name
22+
x509_extensions = v3_req
23+
prompt = no
24+
25+
[req_distinguished_name]
26+
CN = Coder Self Signed EV
27+
28+
[v3_req]
29+
keyUsage = digitalSignature
30+
extendedKeyUsage = codeSigning
31+
certificatePolicies = @pol1,@pol2
32+
33+
[pol1]
34+
policyIdentifier = 2.23.140.1.4.1
35+
CPS.1="https://coder.com"
36+
37+
[pol2]
38+
policyIdentifier = 2.23.140.1.3
39+
CPS.1="https://coder.com"
40+
"
41+
42+
openssl req \
43+
-x509 \
44+
-newkey rsa:2048 \
45+
-keyout /dev/null \
46+
-out self-signed-ev.crt \
47+
-days 3650 \
48+
-nodes \
49+
-config <(echo "$config")
1.94 KB
Binary file not shown.
Binary file not shown.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDcTCCAlmgAwIBAgIUEzhP7oxDynN4aXNmRqGZzPUCkxUwDQYJKoZIhvcNAQEL
3+
BQAwHzEdMBsGA1UEAwwUQ29kZXIgU2VsZiBTaWduZWQgRVYwHhcNMjUwMzA3MDQ0
4+
NzQ2WhcNMzUwMzA1MDQ0NzQ2WjAfMR0wGwYDVQQDDBRDb2RlciBTZWxmIFNpZ25l
5+
ZCBFVjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzzLlfpFYwEfQb6
6+
eizMaUr9/Op2ELLBRjabDT17uXBTPFHVtHewIaZfYPv7aY3B3rQTzHfE0y9YYO0+
7+
1Zhd2WpNKYO3iQM9OOcd69XYuDeRh09m6vOwcK7gSkSr55D/dUe4+vnjQBG9O6Na
8+
fby/kcJuDVNnR9rTPJpXqfnlgjrO2WNbBn3K0xJcNjVpMqFm2Iw9eYCRTVIUp559
9+
+iUGnwM+NT0cGAMB8242Jyz6xgEaRnSmddmxLDkfWWfivamSpWaaopR2T5+6txFW
10+
C1vBeZ4Au+9FEne64NVefVDjdeIDU7pgwYxykPDf+Sc704L8Da5X0gEoa82pHOfw
11+
1DNP94kCAwEAAaOBpDCBoTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
12+
AwMwXgYDVR0gBFcwVTApBgZngQwBBAEwHzAdBggrBgEFBQcCARYRaHR0cHM6Ly9j
13+
b2Rlci5jb20wKAYFZ4EMAQMwHzAdBggrBgEFBQcCARYRaHR0cHM6Ly9jb2Rlci5j
14+
b20wHQYDVR0OBBYEFL9MxyFpCw/CmWioihuEP6x8XW0zMA0GCSqGSIb3DQEBCwUA
15+
A4IBAQC19lsc4EEOD4H3VNtQpLaBPLmNU4dD4bpWpoqv2YIYFl0T2cSkZZ1ZmSKR
16+
PV+1D3w7HsdmOf+1wXQv8w4POy3Z/7m6pcy/Efw9ImYs5zwRr5AniFJxjRBkUYB2
17+
i2m3650v5OAab4qay0FWCY4/8MX866fiLrO0oyjFI6tU/Py8kWV7IgOa9RxJpNou
18+
oITfLXLZRgXULiaXaQRA4TdD5zI9Qe/wwvj6wJH3u8qpRq+m+vo0cxfQ47tisL11
19+
nMM59fUZrypxdOTRK0QiGz5rJlLmZXZO27RNT3ewpJsq4qjQ3CtJ946vdjDc8+kY
20+
ChQ9e6sS5mLBP4JXtuyG+P1Fdp5t
21+
-----END CERTIFICATE-----
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDOzCCAiOgAwIBAgIUJAIgmuCtIc9yUfpTo3h0Srn3C88wDQYJKoZIhvcNAQEL
3+
BQAwHDEaMBgGA1UEAwwRQ29kZXIgU2VsZiBTaWduZWQwHhcNMjUwMzA3MDQ0NzQ2
4+
WhcNMzUwMzA1MDQ0NzQ2WjAcMRowGAYDVQQDDBFDb2RlciBTZWxmIFNpZ25lZDCC
5+
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK6uYLDXy/C7TC4XaUbEmBiK
6+
ICUAz/XDFB19aJrvG6LQYmdbR8sCV4uvoZAXYfribD+AGrysEDnpny1BzNORARLd
7+
szBy0m1fOGbWqaBXg7Ot37rHWkU2iT2NEminHinx9UoJZLWxXsT1h1pnyJO4uzBW
8+
jroRgYbOzkaccoDSWebDjnzAy4LA+Sfdzqm4RvJFD+5dhg/EXyJyLQApN22NWOTP
9+
/8UXO0guUwSC+TGNFGRE6DkN96uX851HaCgrflz9zdLN5FSrSqvTsSStMTF9tHU3
10+
RlZFjTL+pVD6XSRMLn78xbch87sD3egaxaTvKd9Crx88GMwAnmrp8HqFAdUm4WUC
11+
AwEAAaN1MHMwHQYDVR0OBBYEFDJM3fCTrkRAMNKQhoH3mWyLXY0JMB8GA1UdIwQY
12+
MBaAFDJM3fCTrkRAMNKQhoH3mWyLXY0JMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0P
13+
BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQCU
14+
xdNbMynw3YexqDjzHaKFGp4p8pXH9lXAa50e2UsYQ2C3HjBRkDQ0VGwqZaWWidMo
15+
ZdL3TiKT3Gvd+eVEvZNVYAhOytevhztGa7RSIC54KbHM9pQiarQcCCpAVR3S1Ced
16+
zGExk2iVL/o4TZKipvv+lj9b+FmavtoWq9kDuO0Suja0rzBfk5/UpATWbVOSGzY9
17+
1u0Rm2aIGgKxpOVPaxjD8JzJ+47r7Z6tSoYt4PScRy1kgf0VwKeUUdWLiGN4JxoS
18+
dX/onACConyPh4gK1fbHuKaoxVkIV3nFRAk18AwF4jThqFDkCQmUwh7A0DSyiPiD
19+
WeRW1iidZptmwzaOLnwz
20+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)