1+ using System . Reflection ;
12using System . Security . Cryptography ;
3+ using System . Security . Cryptography . X509Certificates ;
24using System . Text ;
35using Coder . Desktop . Vpn . Service ;
46using Microsoft . Extensions . Logging . Abstractions ;
@@ -27,40 +29,102 @@ public class AuthenticodeDownloadValidatorTest
2729 [ CancelAfter ( 30_000 ) ]
2830 public void Unsigned ( CancellationToken ct )
2931 {
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" ) ) ;
3138 }
3239
3340 [ Test ( Description = "Test an untrusted binary" ) ]
3441 [ CancelAfter ( 30_000 ) ]
3542 public void Untrusted ( CancellationToken ct )
3643 {
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" ) ) ;
3851 }
3952
4053 [ Test ( Description = "Test an binary with a detached signature (catalog file)" ) ]
4154 [ CancelAfter ( 30_000 ) ]
4255 public void DifferentCertTrusted ( CancellationToken ct )
4356 {
44- // notepad .exe uses a catalog file for its signature.
57+ // rundll32 .exe uses a catalog file for its signature.
4558 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 ) ) ;
4760 Assert . That ( ex . Message ,
4861 Does . Contain ( "File is not signed with an embedded Authenticode signature: Kind=Catalog" ) ) ;
4962 }
5063
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" ) ]
5278 [ CancelAfter ( 30_000 ) ]
53- public void DifferentCertUntrusted ( CancellationToken ct )
79+ public void EvDifferentCertName ( CancellationToken ct )
5480 {
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.'" ) ) ;
5688 }
5789
5890 [ Test ( Description = "Test a binary signed by Coder's certificate" ) ]
5991 [ CancelAfter ( 30_000 ) ]
6092 public async Task CoderSigned ( CancellationToken ct )
6193 {
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+ }
64128 }
65129}
66130
@@ -71,22 +135,60 @@ public class AssemblyVersionDownloadValidatorTest
71135 [ CancelAfter ( 30_000 ) ]
72136 public void NoVersion ( CancellationToken ct )
73137 {
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" ) ) ;
75153 }
76154
77- [ Test ( Description = "Version mismatch" ) ]
155+ [ Test ( Description = "Version mismatch with full version check " ) ]
78156 [ CancelAfter ( 30_000 ) ]
79- public void VersionMismatch ( CancellationToken ct )
157+ public void VersionMismatchFull ( CancellationToken ct )
80158 {
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+ }
82177 }
83178
84- [ Test ( Description = "Version match" ) ]
179+ [ Test ( Description = "Version match with and without partial version check " ) ]
85180 [ CancelAfter ( 30_000 ) ]
86181 public async Task VersionMatch ( CancellationToken ct )
87182 {
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 ) ;
90192 }
91193}
92194
0 commit comments