1
+ using System . Reflection ;
1
2
using System . Security . Cryptography ;
3
+ using System . Security . Cryptography . X509Certificates ;
2
4
using System . Text ;
3
5
using Coder . Desktop . Vpn . Service ;
4
6
using Microsoft . Extensions . Logging . Abstractions ;
@@ -27,40 +29,102 @@ public class AuthenticodeDownloadValidatorTest
27
29
[ CancelAfter ( 30_000 ) ]
28
30
public void Unsigned ( CancellationToken ct )
29
31
{
30
- // TODO: this
32
+ var testBinaryPath = Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" , "hello.exe" ) ;
33
+ var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
34
+ AuthenticodeDownloadValidator . Coder . ValidateAsync ( testBinaryPath , ct ) ) ;
35
+ Assert . That ( ex . Message ,
36
+ Does . Contain (
37
+ "File is not signed and trusted with an Authenticode signature: State=Unsigned, StateReason=None" ) ) ;
31
38
}
32
39
33
40
[ Test ( Description = "Test an untrusted binary" ) ]
34
41
[ CancelAfter ( 30_000 ) ]
35
42
public void Untrusted ( CancellationToken ct )
36
43
{
37
- // TODO: this
44
+ var testBinaryPath =
45
+ Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" , "hello-self-signed.exe" ) ;
46
+ var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
47
+ AuthenticodeDownloadValidator . Coder . ValidateAsync ( testBinaryPath , ct ) ) ;
48
+ Assert . That ( ex . Message ,
49
+ Does . Contain (
50
+ "File is not signed and trusted with an Authenticode signature: State=Unsigned, StateReason=UntrustedRoot" ) ) ;
38
51
}
39
52
40
53
[ Test ( Description = "Test an binary with a detached signature (catalog file)" ) ]
41
54
[ CancelAfter ( 30_000 ) ]
42
55
public void DifferentCertTrusted ( CancellationToken ct )
43
56
{
44
- // notepad .exe uses a catalog file for its signature.
57
+ // rundll32 .exe uses a catalog file for its signature.
45
58
var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
46
- AuthenticodeDownloadValidator . Coder . ValidateAsync ( @"C:\Windows\System32\notepad .exe" , ct ) ) ;
59
+ AuthenticodeDownloadValidator . Coder . ValidateAsync ( @"C:\Windows\System32\rundll32 .exe" , ct ) ) ;
47
60
Assert . That ( ex . Message ,
48
61
Does . Contain ( "File is not signed with an embedded Authenticode signature: Kind=Catalog" ) ) ;
49
62
}
50
63
51
- [ Test ( Description = "Test a binary signed by a different certificate" ) ]
64
+ [ Test ( Description = "Test a binary signed by a non-EV certificate" ) ]
65
+ [ CancelAfter ( 30_000 ) ]
66
+ public void NonEvCert ( CancellationToken ct )
67
+ {
68
+ // dotnet.exe is signed by .NET. During tests we can be pretty sure
69
+ // this is installed.
70
+ var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
71
+ AuthenticodeDownloadValidator . Coder . ValidateAsync ( @"C:\Program Files\dotnet\dotnet.exe" , ct ) ) ;
72
+ Assert . That ( ex . Message ,
73
+ Does . Contain (
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" ) ]
52
78
[ CancelAfter ( 30_000 ) ]
53
- public void DifferentCertUntrusted ( CancellationToken ct )
79
+ public void EvDifferentCertName ( CancellationToken ct )
54
80
{
55
- // TODO: this
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.'" ) ) ;
56
88
}
57
89
58
90
[ Test ( Description = "Test a binary signed by Coder's certificate" ) ]
59
91
[ CancelAfter ( 30_000 ) ]
60
92
public async Task CoderSigned ( CancellationToken ct )
61
93
{
62
- // TODO: this
63
- await Task . CompletedTask ;
94
+ var testBinaryPath = Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" ,
95
+ "hello-versioned-signed.exe" ) ;
96
+ await AuthenticodeDownloadValidator . Coder . ValidateAsync ( testBinaryPath , ct ) ;
97
+ }
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
+ }
64
128
}
65
129
}
66
130
@@ -71,22 +135,60 @@ public class AssemblyVersionDownloadValidatorTest
71
135
[ CancelAfter ( 30_000 ) ]
72
136
public void NoVersion ( CancellationToken ct )
73
137
{
74
- // TODO: this
138
+ var testBinaryPath = Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" , "hello.exe" ) ;
139
+ var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
140
+ new AssemblyVersionDownloadValidator ( 1 , 2 , 3 , 4 ) . ValidateAsync ( testBinaryPath , ct ) ) ;
141
+ Assert . That ( ex . Message , Does . Contain ( "File ProductVersion is empty or null" ) ) ;
142
+ }
143
+
144
+ [ Test ( Description = "Invalid version on binary" ) ]
145
+ [ CancelAfter ( 30_000 ) ]
146
+ public void InvalidVersion ( CancellationToken ct )
147
+ {
148
+ var testBinaryPath =
149
+ Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" , "hello-invalid-version.exe" ) ;
150
+ var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
151
+ new AssemblyVersionDownloadValidator ( 1 , 2 , 3 , 4 ) . ValidateAsync ( testBinaryPath , ct ) ) ;
152
+ Assert . That ( ex . Message , Does . Contain ( "File ProductVersion '1-2-3-4' is not a valid version string" ) ) ;
75
153
}
76
154
77
- [ Test ( Description = "Version mismatch" ) ]
155
+ [ Test ( Description = "Version mismatch with full version check " ) ]
78
156
[ CancelAfter ( 30_000 ) ]
79
- public void VersionMismatch ( CancellationToken ct )
157
+ public void VersionMismatchFull ( CancellationToken ct )
80
158
{
81
- // TODO: this
159
+ var testBinaryPath = Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" ,
160
+ "hello-versioned-signed.exe" ) ;
161
+
162
+ // Try changing each version component one at a time
163
+ var expectedVersions = new [ ] { 1 , 2 , 3 , 4 } ;
164
+ for ( var i = 0 ; i < 4 ; i ++ )
165
+ {
166
+ var testVersions = ( int [ ] ) expectedVersions . Clone ( ) ;
167
+ testVersions [ i ] ++ ; // Increment this component to make it wrong
168
+
169
+ var ex = Assert . ThrowsAsync < Exception > ( ( ) =>
170
+ new AssemblyVersionDownloadValidator (
171
+ testVersions [ 0 ] , testVersions [ 1 ] , testVersions [ 2 ] , testVersions [ 3 ]
172
+ ) . ValidateAsync ( testBinaryPath , ct ) ) ;
173
+
174
+ Assert . That ( ex . Message , Does . Contain (
175
+ $ "File ProductVersion does not match expected version: Actual='1.2.3.4', Expected='{ string . Join ( "." , testVersions ) } '") ) ;
176
+ }
82
177
}
83
178
84
- [ Test ( Description = "Version match" ) ]
179
+ [ Test ( Description = "Version match with and without partial version check " ) ]
85
180
[ CancelAfter ( 30_000 ) ]
86
181
public async Task VersionMatch ( CancellationToken ct )
87
182
{
88
- // TODO: this
89
- await Task . CompletedTask ;
183
+ var testBinaryPath = Path . Combine ( TestContext . CurrentContext . TestDirectory , "testdata" ,
184
+ "hello-versioned-signed.exe" ) ;
185
+
186
+ // Test with just major.minor
187
+ await new AssemblyVersionDownloadValidator ( 1 , 2 ) . ValidateAsync ( testBinaryPath , ct ) ;
188
+ // Test with major.minor.patch
189
+ await new AssemblyVersionDownloadValidator ( 1 , 2 , 3 ) . ValidateAsync ( testBinaryPath , ct ) ;
190
+ // Test with major.minor.patch.build
191
+ await new AssemblyVersionDownloadValidator ( 1 , 2 , 3 , 4 ) . ValidateAsync ( testBinaryPath , ct ) ;
90
192
}
91
193
}
92
194
0 commit comments