1818using System . Text ;
1919using Keyfactor . Extensions . CAPlugin . Acme . Clients . DNS ;
2020using System . Text . RegularExpressions ;
21+ using Org . BouncyCastle . Asn1 ;
22+ using Org . BouncyCastle . Asn1 . Pkcs ;
23+ using Org . BouncyCastle . Asn1 . X509 ;
24+ using Org . BouncyCastle . Pkcs ;
2125
2226namespace Keyfactor . Extensions . CAPlugin . Acme
2327{
@@ -248,8 +252,12 @@ public async Task<EnrollmentResult> Enroll(
248252 var acmeClient = new AcmeClient ( _logger , config , httpClient , protocolClient . Directory ,
249253 new Clients . Acme . Account ( accountDetails , signer ) ) ;
250254
251- // Extract all domains (CN + SANs) for the ACME order
252- var identifiers = BuildIdentifiersFromSubjectAndSan ( subject , san ) ;
255+ // Decode CSR first so we can extract all domains from it
256+ var csrBytes = Convert . FromBase64String ( csr ) ;
257+
258+ // Extract all domains directly from CSR (CN + SANs) for the ACME order
259+ // This ensures we authorize exactly what's in the CSR
260+ var identifiers = ExtractDomainsFromCsr ( csrBytes ) ;
253261
254262 // Create order
255263 var order = await acmeClient . CreateOrderAsync ( identifiers , null ) ;
@@ -260,8 +268,7 @@ public async Task<EnrollmentResult> Enroll(
260268 // Process challenges
261269 await ProcessAuthorizations ( acmeClient , order , config ) ;
262270
263- // Finalize
264- var csrBytes = Convert . FromBase64String ( csr ) ;
271+ // Finalize with original CSR bytes
265272 order = await acmeClient . FinalizeOrderAsync ( order , csrBytes ) ;
266273
267274 // If order is valid immediately, download cert
@@ -328,43 +335,81 @@ private static string ExtractDomainFromSubject(string subject)
328335 }
329336
330337 /// <summary>
331- /// Builds ACME identifiers from subject CN and SANs.
332- /// ACME orders must include all domains that will be in the CSR.
338+ /// Extracts all DNS names ( CN + SANs) directly from the CSR .
339+ /// This ensures the ACME order authorizes exactly what's in the CSR.
333340 /// </summary>
334- /// <param name="subject">Subject string containing CN</param>
335- /// <param name="san">Dictionary of SANs (key: type like "dns", value: array of names)</param>
336- /// <returns>List of unique ACME identifiers for all domains</returns>
337- private List < Identifier > BuildIdentifiersFromSubjectAndSan ( string subject , Dictionary < string , string [ ] > san )
341+ /// <param name="csrBytes">DER-encoded CSR bytes</param>
342+ /// <returns>List of ACME identifiers for all domains in the CSR</returns>
343+ private List < Identifier > ExtractDomainsFromCsr ( byte [ ] csrBytes )
338344 {
339345 var domains = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
340346
341- // Add the CN from subject
342- var cnDomain = ExtractDomainFromSubject ( subject ) ;
343- domains . Add ( cnDomain ) ;
344- _logger . LogDebug ( "Added CN domain to identifiers: {Domain}" , cnDomain ) ;
345-
346- // Add DNS SANs if present
347- if ( san != null )
347+ try
348348 {
349- // Check for "dns" key (case-insensitive)
350- foreach ( var kvp in san )
349+ // Parse the CSR using BouncyCastle
350+ var pkcs10 = new Pkcs10CertificationRequest ( csrBytes ) ;
351+ var csrInfo = pkcs10 . GetCertificationRequestInfo ( ) ;
352+
353+ // Extract CN from subject
354+ var subject = csrInfo . Subject ;
355+ var cnValues = subject . GetValueList ( X509Name . CN ) ;
356+ if ( cnValues != null && cnValues . Count > 0 )
351357 {
352- if ( kvp . Key . Equals ( "dns" , StringComparison . OrdinalIgnoreCase ) && kvp . Value != null )
358+ var cn = cnValues [ 0 ] ? . ToString ( ) ;
359+ if ( ! string . IsNullOrWhiteSpace ( cn ) )
353360 {
354- foreach ( var dnsName in kvp . Value )
361+ domains . Add ( cn ) ;
362+ _logger . LogDebug ( "Extracted CN from CSR: {Domain}" , cn ) ;
363+ }
364+ }
365+
366+ // Extract SANs from CSR attributes
367+ var attributes = csrInfo . Attributes ;
368+ if ( attributes != null )
369+ {
370+ foreach ( var attr in attributes )
371+ {
372+ var attribute = Org . BouncyCastle . Asn1 . Pkcs . AttributePkcs . GetInstance ( attr ) ;
373+ if ( attribute . AttrType . Equals ( PkcsObjectIdentifiers . Pkcs9AtExtensionRequest ) )
355374 {
356- if ( ! string . IsNullOrWhiteSpace ( dnsName ) )
375+ // This attribute contains extension requests
376+ var extensions = X509Extensions . GetInstance ( attribute . AttrValues [ 0 ] ) ;
377+ var sanExtension = extensions . GetExtension ( X509Extensions . SubjectAlternativeName ) ;
378+
379+ if ( sanExtension != null )
357380 {
358- domains . Add ( dnsName . Trim ( ) ) ;
359- _logger . LogDebug ( "Added SAN domain to identifiers: {Domain}" , dnsName . Trim ( ) ) ;
381+ var sanNames = GeneralNames . GetInstance ( sanExtension . GetParsedValue ( ) ) ;
382+ foreach ( var name in sanNames . GetNames ( ) )
383+ {
384+ // TagNo 2 = dNSName
385+ if ( name . TagNo == GeneralName . DnsName )
386+ {
387+ var dnsName = name . Name . ToString ( ) ;
388+ if ( ! string . IsNullOrWhiteSpace ( dnsName ) )
389+ {
390+ domains . Add ( dnsName ) ;
391+ _logger . LogDebug ( "Extracted SAN from CSR: {Domain}" , dnsName ) ;
392+ }
393+ }
394+ }
360395 }
361396 }
362397 }
363398 }
364399 }
400+ catch ( Exception ex )
401+ {
402+ _logger . LogError ( ex , "Failed to parse CSR for domain extraction" ) ;
403+ throw new InvalidOperationException ( "Failed to parse CSR to extract domains" , ex ) ;
404+ }
405+
406+ if ( domains . Count == 0 )
407+ {
408+ throw new InvalidOperationException ( "No DNS names found in CSR (neither CN nor SANs)" ) ;
409+ }
365410
366411 var identifiers = domains . Select ( d => new Identifier { Type = "dns" , Value = d } ) . ToList ( ) ;
367- _logger . LogInformation ( "Created ACME order with {Count} identifier (s): {Domains}" ,
412+ _logger . LogInformation ( "Extracted {Count} domain (s) from CSR : {Domains}" ,
368413 identifiers . Count , string . Join ( ", " , domains ) ) ;
369414
370415 return identifiers ;
0 commit comments