@@ -172,6 +172,32 @@ exports.ParseMLEConfigString = function (configString, logger) {
172172 }
173173}
174174
175+ /**
176+ * Internal helper: Parses a P12 file and returns the pkcs12 object
177+ * @param {string } filePath - Path to the P12 file
178+ * @param {string } password - Password for the P12 file
179+ * @param {object } logger - Logger object for logging messages
180+ * @returns {object } - Parsed pkcs12 object
181+ * @throws Will throw an error if file reading or parsing fails
182+ */
183+ function parseP12File ( filePath , password , logger ) {
184+ logger . debug ( `Parsing P12 file: ${ filePath } ` ) ;
185+
186+ if ( ! fs . existsSync ( filePath ) ) {
187+ logger . error ( `File not found: ${ filePath } ` ) ;
188+ throw new Error ( Constants . FILE_NOT_FOUND + filePath ) ;
189+ }
190+
191+ var p12Buffer = fs . readFileSync ( filePath ) ;
192+ var p12Der = forge . util . binary . raw . encode ( new Uint8Array ( p12Buffer ) ) ;
193+ var p12Asn1 = forge . asn1 . fromDer ( p12Der ) ;
194+ var p12 = forge . pkcs12 . pkcs12FromAsn1 ( p12Asn1 , false , password ) ;
195+
196+ logger . debug ( `Successfully parsed P12 file` ) ;
197+
198+ return p12 ;
199+ }
200+
175201/**
176202 * Reads a private key from a P12 file
177203 * @param {string } filePath - Path to the P12 file
@@ -183,18 +209,7 @@ exports.readPrivateKeyFromP12 = function(filePath, password, logger) {
183209 try {
184210 logger . debug ( `Reading private key from P12 file: ${ filePath } ` ) ;
185211
186- if ( ! fs . existsSync ( filePath ) ) {
187- logger . error ( `File not found: ${ filePath } ` ) ;
188- ApiException . AuthException ( Constants . FILE_NOT_FOUND + filePath ) ;
189- }
190-
191- // Read the P12 file and convert to ASN1
192- var p12Buffer = fs . readFileSync ( filePath ) ;
193- var p12Der = forge . util . binary . raw . encode ( new Uint8Array ( p12Buffer ) ) ;
194- var p12Asn1 = forge . asn1 . fromDer ( p12Der ) ;
195- var p12 = forge . pkcs12 . pkcs12FromAsn1 ( p12Asn1 , false , password ) ;
196-
197- logger . debug ( `Successfully read P12 file and converted to ASN1` ) ;
212+ var p12 = parseP12File ( filePath , password , logger ) ;
198213
199214 // Extract the private key
200215 var keyBags = p12 . getBags ( { bagType : forge . pki . oids . keyBag } ) ;
@@ -357,3 +372,169 @@ exports.parseAndReturnPem = function(key, logger, password, passwordPropertyName
357372 throw new Error ( 'Unsupported key format' ) ;
358373 }
359374}
375+
376+ /**
377+ * Checks if a P12 file is generated by CyberSource
378+ * Validates that the P12 contains a certificate with CN="CyberSource_SJC_US" and only one private key
379+ * @param {string } filePath - Path to the P12 file
380+ * @param {string } password - Password for the P12 file
381+ * @param {object } logger - Logger object for logging messages
382+ * @returns {boolean } - True if the P12 file is generated by CyberSource, false otherwise
383+ */
384+ exports . isCybersourceP12 = function ( filePath , password , logger ) {
385+ try {
386+ logger . debug ( `Checking if P12 file is generated by CyberSource: ${ filePath } ` ) ;
387+
388+ const p12 = parseP12File ( filePath , password , logger ) ;
389+ const certBags = p12 . getBags ( { bagType : forge . pki . oids . certBag } ) ;
390+ const certs = certBags [ forge . pki . oids . certBag ] ;
391+
392+ // Early return if no certificates found
393+ if ( ! certs ) {
394+ logger . debug ( 'No certificates found in P12 file' ) ;
395+ return false ;
396+ }
397+
398+ logger . debug ( `Found ${ certs . length } certificate(s) in P12 file` ) ;
399+
400+ // Check for CyberSource certificate using modern iteration
401+ const hasCybersourceCert = certs . some ( ( { cert } ) => {
402+ if ( ! cert ?. subject ?. attributes ) return false ;
403+
404+ const cnAttr = cert . subject . attributes . find (
405+ attr => attr . name === 'commonName' || attr . shortName === 'CN'
406+ ) ;
407+
408+ if ( cnAttr ) {
409+ logger . debug ( `Found certificate with CN: ${ cnAttr . value } ` ) ;
410+ if ( cnAttr . value === Constants . CYBERSOURCE_P12_CERT_ALIAS ) {
411+ logger . debug ( `Found CyberSource certificate (CN=${ Constants . CYBERSOURCE_P12_CERT_ALIAS } )` ) ;
412+ return true ;
413+ }
414+ }
415+ return false ;
416+ } ) ;
417+
418+ if ( ! hasCybersourceCert ) {
419+ logger . debug ( `P12 file does not contain CyberSource certificate (CN=${ Constants . CYBERSOURCE_P12_CERT_ALIAS } )` ) ;
420+ return false ;
421+ }
422+
423+ // Count private keys from both bag types
424+ const bagTypes = [
425+ { oid : forge . pki . oids . keyBag , name : 'keyBag' } ,
426+ { oid : forge . pki . oids . pkcs8ShroudedKeyBag , name : 'pkcs8ShroudedKeyBag' }
427+ ] ;
428+
429+ let privateKeyCount = 0 ;
430+ for ( const { oid, name } of bagTypes ) {
431+ const bags = p12 . getBags ( { bagType : oid } ) ;
432+ const count = bags [ oid ] ?. length || 0 ;
433+ if ( count > 0 ) {
434+ privateKeyCount += count ;
435+ logger . debug ( `Found ${ count } ${ name } private key(s)` ) ;
436+ }
437+ }
438+
439+ logger . debug ( `Total private keys found: ${ privateKeyCount } ` ) ;
440+
441+ // Verify exactly one private key
442+ if ( privateKeyCount !== 1 ) {
443+ logger . debug ( `P12 file does not contain exactly one private key (found ${ privateKeyCount } )` ) ;
444+ return false ;
445+ }
446+
447+ logger . debug ( 'P12 file is generated by CyberSource: contains CyberSource certificate and exactly one private key' ) ;
448+ return true ;
449+
450+ } catch ( error ) {
451+ logger . error ( `Error checking if P12 file is generated by CyberSource: ${ error . message } ` ) ;
452+ return false ;
453+ }
454+ } ;
455+
456+ /**
457+ * Extracts the serial number (KID) from a certificate's subject in a P12 file where CN matches the merchantId
458+ * @param {string } filePath - Path to the P12 file
459+ * @param {string } password - Password for the P12 file
460+ * @param {string } merchantId - The merchant ID to match against the CN in the certificate subject
461+ * @param {object } logger - Logger object for logging messages
462+ * @returns {string } - The serial number extracted from the certificate's subject attributes
463+ * @throws Will throw an error if the certificate with matching CN is not found or serial number is missing
464+ */
465+ exports . extractResponseMleKid = function ( filePath , password , merchantId , logger ) {
466+ try {
467+ logger . debug ( `Extracting MLE KID from P12 file: ${ filePath } for merchantId: ${ merchantId } ` ) ;
468+
469+ const p12 = parseP12File ( filePath , password , logger ) ;
470+
471+ // Get certificate bags from P12
472+ const certBags = p12 . getBags ( { bagType : forge . pki . oids . certBag } ) ;
473+ const certs = certBags [ forge . pki . oids . certBag ] ;
474+
475+ if ( ! certs || certs . length === 0 ) {
476+ logger . error ( `No certificates found in P12 file: ${ filePath } ` ) ;
477+ ApiException . AuthException ( `No certificates found in P12 file: ${ filePath } ` ) ;
478+ }
479+
480+ logger . debug ( `Found ${ certs . length } certificate(s) in P12 file` ) ;
481+
482+ // Iterate through certificates to find one with matching CN
483+ for ( let i = 0 ; i < certs . length ; i ++ ) {
484+ const certBag = certs [ i ] ;
485+ const cert = certBag . cert ;
486+
487+ if ( ! cert || ! cert . subject || ! cert . subject . attributes ) {
488+ logger . debug ( `Certificate ${ i + 1 } has no subject attributes, skipping` ) ;
489+ continue ;
490+ }
491+
492+ // Extract CN from certificate subject
493+ let cn = null ;
494+ for ( const attr of cert . subject . attributes ) {
495+ if ( attr . name === 'commonName' || attr . shortName === 'CN' ) {
496+ cn = attr . value ;
497+ break ;
498+ }
499+ }
500+
501+ if ( ! cn ) {
502+ logger . debug ( `Certificate ${ i + 1 } has no CN in subject, skipping` ) ;
503+ continue ;
504+ }
505+
506+ logger . debug ( `Certificate ${ i + 1 } CN: ${ cn } ` ) ;
507+
508+ // Check if CN matches merchantId (case-insensitive)
509+ if ( cn . toLowerCase ( ) === merchantId . toLowerCase ( ) ) {
510+ logger . debug ( `Found certificate with matching CN: ${ cn } ` ) ;
511+
512+ // Extract serial number from certificate subject
513+ let serialNumber = null ;
514+ for ( const attr of cert . subject . attributes ) {
515+ if ( attr . name === 'serialNumber' || attr . shortName === 'serialNumber' ) {
516+ serialNumber = attr . value ;
517+ break ;
518+ }
519+ }
520+
521+ if ( ! serialNumber ) {
522+ logger . debug ( `Certificate with CN=${ cn } has no serialNumber in subject, continuing search` ) ;
523+ continue ;
524+ }
525+
526+ logger . debug ( `Serial number (MLE KID) extracted from certificate subject: ${ serialNumber } ` ) ;
527+
528+ return serialNumber ;
529+ }
530+ }
531+
532+ // If we get here, no matching certificate was found
533+ logger . error ( `No certificate with CN matching merchantId (${ merchantId } ) and valid serialNumber found in P12 file: ${ filePath } ` ) ;
534+ ApiException . AuthException ( `No certificate with CN matching merchantId (${ merchantId } ) found in P12 file: ${ filePath } ` ) ;
535+
536+ } catch ( error ) {
537+ logger . error ( `Error extracting MLE KID from P12 file: ${ filePath } : ${ error . message } ` ) ;
538+ ApiException . AuthException ( `Error extracting MLE KID from P12 file: ${ filePath } : ${ error . message } ` ) ;
539+ }
540+ } ;
0 commit comments