1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ using System ;
5+ using System . IO ;
6+ using System . Security . Cryptography ;
7+ using System . Security . Cryptography . X509Certificates ;
8+ using Agent . Sdk . Util ;
9+ using Xunit ;
10+
11+ namespace Microsoft . VisualStudio . Services . Agent . Tests . Util
12+ {
13+ /// <summary>
14+ /// Tests for CertificateUtil.LoadCertificate which works on both .NET 8 and .NET 10.
15+ /// Tests cover: Cert (DER/PEM) and PFX/PKCS#12 formats.
16+ /// </summary>
17+ public sealed class CertificateUtilL0 : IDisposable
18+ {
19+ private readonly string _tempDir ;
20+
21+ public CertificateUtilL0 ( )
22+ {
23+ _tempDir = Path . Combine ( Path . GetTempPath ( ) , $ "CertUtilTests_{ Guid . NewGuid ( ) : N} ") ;
24+ Directory . CreateDirectory ( _tempDir ) ;
25+ }
26+
27+ public void Dispose ( )
28+ {
29+ if ( Directory . Exists ( _tempDir ) )
30+ {
31+ Directory . Delete ( _tempDir , recursive : true ) ;
32+ }
33+ }
34+
35+ #region PFX/PKCS#12 Tests (X509ContentType.Pkcs12)
36+
37+ [ Fact ]
38+ [ Trait ( "Level" , "L0" ) ]
39+ [ Trait ( "Category" , "Common" ) ]
40+ public void LoadCertificate_Pfx_WithPassword_LoadsSuccessfully ( )
41+ {
42+ // Arrange
43+ var ( expectedThumbprint , pfxPath ) = CreatePfxCertificate ( "test-password" ) ;
44+
45+ // Act
46+ using var loadedCert = CertificateUtil . LoadCertificate ( pfxPath , "test-password" ) ;
47+
48+ // Assert
49+ Assert . NotNull ( loadedCert ) ;
50+ Assert . Equal ( expectedThumbprint , loadedCert . Thumbprint ) ;
51+ }
52+
53+ [ Fact ]
54+ [ Trait ( "Level" , "L0" ) ]
55+ [ Trait ( "Category" , "Common" ) ]
56+ public void LoadCertificate_Pfx_WithoutPassword_LoadsSuccessfully ( )
57+ {
58+ // Arrange
59+ var ( expectedThumbprint , pfxPath ) = CreatePfxCertificate ( password : null ) ;
60+
61+ // Act
62+ using var loadedCert = CertificateUtil . LoadCertificate ( pfxPath , password : null ) ;
63+
64+ // Assert
65+ Assert . NotNull ( loadedCert ) ;
66+ Assert . Equal ( expectedThumbprint , loadedCert . Thumbprint ) ;
67+ }
68+
69+ [ Fact ]
70+ [ Trait ( "Level" , "L0" ) ]
71+ [ Trait ( "Category" , "Common" ) ]
72+ public void LoadCertificate_Pfx_WrongPassword_ThrowsException ( )
73+ {
74+ // Arrange
75+ var ( _, pfxPath ) = CreatePfxCertificate ( "correct-password" ) ;
76+
77+ // Act & Assert
78+ Assert . ThrowsAny < CryptographicException > ( ( ) =>
79+ CertificateUtil . LoadCertificate ( pfxPath , "wrong-password" ) ) ;
80+ }
81+
82+ [ Fact ]
83+ [ Trait ( "Level" , "L0" ) ]
84+ [ Trait ( "Category" , "Common" ) ]
85+ public void LoadCertificate_Pfx_PasswordProtectedButNoPasswordProvided_ThrowsException ( )
86+ {
87+ // Arrange
88+ var ( _, pfxPath ) = CreatePfxCertificate ( "some-password" ) ;
89+
90+ // Act & Assert
91+ Assert . ThrowsAny < CryptographicException > ( ( ) =>
92+ CertificateUtil . LoadCertificate ( pfxPath , password : null ) ) ;
93+ }
94+
95+ #endregion
96+
97+ #region DER Tests (X509ContentType.Cert)
98+
99+ [ Fact ]
100+ [ Trait ( "Level" , "L0" ) ]
101+ [ Trait ( "Category" , "Common" ) ]
102+ public void LoadCertificate_Der_LoadsSuccessfully ( )
103+ {
104+ // Arrange
105+ var ( expectedThumbprint , derPath ) = CreateDerCertificate ( ) ;
106+
107+ // Act
108+ using var loadedCert = CertificateUtil . LoadCertificate ( derPath ) ;
109+
110+ // Assert
111+ Assert . NotNull ( loadedCert ) ;
112+ Assert . Equal ( expectedThumbprint , loadedCert . Thumbprint ) ;
113+ }
114+
115+ #endregion
116+
117+ #region PEM Tests (X509ContentType.Cert)
118+
119+ [ Fact ]
120+ [ Trait ( "Level" , "L0" ) ]
121+ [ Trait ( "Category" , "Common" ) ]
122+ public void LoadCertificate_Pem_LoadsSuccessfully ( )
123+ {
124+ // Arrange
125+ var ( expectedThumbprint , pemPath ) = CreatePemCertificate ( ) ;
126+
127+ // Act
128+ using var loadedCert = CertificateUtil . LoadCertificate ( pemPath ) ;
129+
130+ // Assert
131+ Assert . NotNull ( loadedCert ) ;
132+ Assert . Equal ( expectedThumbprint , loadedCert . Thumbprint ) ;
133+ }
134+
135+ #endregion
136+
137+ #region Helper Methods
138+
139+ /// <summary>
140+ /// Creates a test PFX/PKCS#12 certificate file (X509ContentType.Pkcs12).
141+ /// </summary>
142+ private ( string thumbprint , string path ) CreatePfxCertificate ( string password )
143+ {
144+ using var rsa = RSA . Create ( 2048 ) ;
145+ var request = new CertificateRequest (
146+ "CN=TestPfxCertificate" ,
147+ rsa ,
148+ HashAlgorithmName . SHA256 ,
149+ RSASignaturePadding . Pkcs1 ) ;
150+
151+ using var cert = request . CreateSelfSigned (
152+ DateTimeOffset . UtcNow . AddMinutes ( - 5 ) ,
153+ DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
154+
155+ var pfxPath = Path . Combine ( _tempDir , $ "test_{ Guid . NewGuid ( ) : N} .pfx") ;
156+ var pfxBytes = cert . Export ( X509ContentType . Pfx , password ) ;
157+ File . WriteAllBytes ( pfxPath , pfxBytes ) ;
158+
159+ return ( cert . Thumbprint , pfxPath ) ;
160+ }
161+
162+ /// <summary>
163+ /// Creates a test DER-encoded certificate file (X509ContentType.Cert).
164+ /// </summary>
165+ private ( string thumbprint , string path ) CreateDerCertificate ( )
166+ {
167+ using var rsa = RSA . Create ( 2048 ) ;
168+ var request = new CertificateRequest (
169+ "CN=TestDerCertificate" ,
170+ rsa ,
171+ HashAlgorithmName . SHA256 ,
172+ RSASignaturePadding . Pkcs1 ) ;
173+
174+ using var cert = request . CreateSelfSigned (
175+ DateTimeOffset . UtcNow . AddMinutes ( - 5 ) ,
176+ DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
177+
178+ var derPath = Path . Combine ( _tempDir , $ "test_{ Guid . NewGuid ( ) : N} .cer") ;
179+ var derBytes = cert . Export ( X509ContentType . Cert ) ;
180+ File . WriteAllBytes ( derPath , derBytes ) ;
181+
182+ return ( cert . Thumbprint , derPath ) ;
183+ }
184+
185+ /// <summary>
186+ /// Creates a test PEM-encoded certificate file.
187+ /// </summary>
188+ private ( string thumbprint , string path ) CreatePemCertificate ( )
189+ {
190+ using var rsa = RSA . Create ( 2048 ) ;
191+ var request = new CertificateRequest (
192+ "CN=TestPemCertificate" ,
193+ rsa ,
194+ HashAlgorithmName . SHA256 ,
195+ RSASignaturePadding . Pkcs1 ) ;
196+
197+ using var cert = request . CreateSelfSigned (
198+ DateTimeOffset . UtcNow . AddMinutes ( - 5 ) ,
199+ DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
200+
201+ var pemPath = Path . Combine ( _tempDir , $ "test_{ Guid . NewGuid ( ) : N} .pem") ;
202+ var pemContent = cert . ExportCertificatePem ( ) ;
203+ File . WriteAllText ( pemPath , pemContent ) ;
204+
205+ return ( cert . Thumbprint , pemPath ) ;
206+ }
207+
208+ #endregion
209+ }
210+ }
0 commit comments