@@ -202,7 +202,7 @@ public class CertPemManager
202202 /// <summary>
203203 /// Get certificate in PEM format from a server with CA pinning validation
204204 /// </summary>
205- public async Task < ( string ? , string ? ) > GetCertPemAsync ( string target , string serverName , int timeout = 10 )
205+ public async Task < ( string ? , string ? ) > GetCertPemAsync ( string target , string serverName , int timeout = 4 )
206206 {
207207 try
208208 {
@@ -216,7 +216,13 @@ public class CertPemManager
216216
217217 using var ssl = new SslStream ( client . GetStream ( ) , false , ValidateServerCertificate ) ;
218218
219- await ssl . AuthenticateAsClientAsync ( serverName ) ;
219+ var sslOptions = new SslClientAuthenticationOptions
220+ {
221+ TargetHost = serverName ,
222+ RemoteCertificateValidationCallback = ValidateServerCertificate
223+ } ;
224+
225+ await ssl . AuthenticateAsClientAsync ( sslOptions , cts . Token ) ;
220226
221227 var remote = ssl . RemoteCertificate ;
222228 if ( remote == null )
@@ -242,7 +248,7 @@ public class CertPemManager
242248 /// <summary>
243249 /// Get certificate chain in PEM format from a server with CA pinning validation
244250 /// </summary>
245- public async Task < ( List < string > , string ? ) > GetCertChainPemAsync ( string target , string serverName , int timeout = 10 )
251+ public async Task < ( List < string > , string ? ) > GetCertChainPemAsync ( string target , string serverName , int timeout = 4 )
246252 {
247253 var pemList = new List < string > ( ) ;
248254 try
@@ -257,7 +263,13 @@ public class CertPemManager
257263
258264 using var ssl = new SslStream ( client . GetStream ( ) , false , ValidateServerCertificate ) ;
259265
260- await ssl . AuthenticateAsClientAsync ( serverName ) ;
266+ var sslOptions = new SslClientAuthenticationOptions
267+ {
268+ TargetHost = serverName ,
269+ RemoteCertificateValidationCallback = ValidateServerCertificate
270+ } ;
271+
272+ await ssl . AuthenticateAsClientAsync ( sslOptions , cts . Token ) ;
261273
262274 if ( ssl . RemoteCertificate is not X509Certificate2 certChain )
263275 {
@@ -330,10 +342,74 @@ private bool ValidateServerCertificate(
330342 return TrustedCaThumbprints . Contains ( rootThumbprint ) ;
331343 }
332344
333- public string ExportCertToPem ( X509Certificate2 cert )
345+ public static string ExportCertToPem ( X509Certificate2 cert )
334346 {
335347 var der = cert . Export ( X509ContentType . Cert ) ;
336- var b64 = Convert . ToBase64String ( der , Base64FormattingOptions . InsertLineBreaks ) ;
348+ var b64 = Convert . ToBase64String ( der ) ;
337349 return $ "-----BEGIN CERTIFICATE-----\n { b64 } \n -----END CERTIFICATE-----\n ";
338350 }
351+
352+ /// <summary>
353+ /// Parse concatenated PEM certificates string into a list of individual certificates
354+ /// Normalizes format: removes line breaks from base64 content for better compatibility
355+ /// </summary>
356+ /// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
357+ /// <returns>List of individual PEM certificate strings with normalized format</returns>
358+ public static List < string > ParsePemChain ( string pemChain )
359+ {
360+ var certs = new List < string > ( ) ;
361+ if ( string . IsNullOrWhiteSpace ( pemChain ) )
362+ {
363+ return certs ;
364+ }
365+
366+ // Normalize line endings (CRLF -> LF) at the beginning
367+ pemChain = pemChain . Replace ( "\r \n " , "\n " ) . Replace ( "\r " , "\n " ) ;
368+
369+ const string beginMarker = "-----BEGIN CERTIFICATE-----" ;
370+ const string endMarker = "-----END CERTIFICATE-----" ;
371+
372+ var index = 0 ;
373+ while ( index < pemChain . Length )
374+ {
375+ var beginIndex = pemChain . IndexOf ( beginMarker , index , StringComparison . Ordinal ) ;
376+ if ( beginIndex == - 1 )
377+ break ;
378+
379+ var endIndex = pemChain . IndexOf ( endMarker , beginIndex , StringComparison . Ordinal ) ;
380+ if ( endIndex == - 1 )
381+ break ;
382+
383+ // Extract certificate content
384+ var base64Start = beginIndex + beginMarker . Length ;
385+ var base64Content = pemChain . Substring ( base64Start , endIndex - base64Start ) ;
386+
387+ // Remove all whitespace from base64 content
388+ base64Content = new string ( base64Content . Where ( c => ! char . IsWhiteSpace ( c ) ) . ToArray ( ) ) ;
389+
390+ // Reconstruct with clean format: BEGIN marker + base64 (no line breaks) + END marker
391+ var normalizedCert = $ "{ beginMarker } \n { base64Content } \n { endMarker } \n ";
392+ certs . Add ( normalizedCert ) ;
393+
394+ // Move to next certificate
395+ index = endIndex + endMarker . Length ;
396+ }
397+
398+ return certs ;
399+ }
400+
401+ /// <summary>
402+ /// Concatenate a list of PEM certificates into a single string
403+ /// </summary>
404+ /// <param name="pemList">List of individual PEM certificate strings</param>
405+ /// <returns>Concatenated PEM certificates string</returns>
406+ public static string ConcatenatePemChain ( IEnumerable < string > pemList )
407+ {
408+ if ( pemList == null )
409+ {
410+ return string . Empty ;
411+ }
412+
413+ return string . Concat ( pemList ) ;
414+ }
339415}
0 commit comments