Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions lib/Authentication/Core/MerchantConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -1734,14 +1734,6 @@ private function validateMLEConfiguration(){
throw $exception;
}

// Validate responseMleKID
if (empty(trim($this->responseMleKID))) {
$error_message = "Response MLE enabled but responseMleKID is not set.";
$exception = new AuthException($error_message, 0);
self::$logger->error($error_message);
throw $exception;
}

$hasFilePath = !empty($this->responseMlePrivateKeyFilePath);
$hasInMemoryKey = !empty($this->responseMlePrivateKey);

Expand All @@ -1758,7 +1750,7 @@ private function validateMLEConfiguration(){
self::$logger->error($error_message);
throw $exception;
}

$isP12File = false;
if ($hasFilePath) {
if (
!file_exists($this->responseMlePrivateKeyFilePath) ||
Expand All @@ -1770,6 +1762,10 @@ private function validateMLEConfiguration(){
self::$logger->error($error_message);
throw $exception;
}
$ext = pathinfo($this->responseMlePrivateKeyFilePath, PATHINFO_EXTENSION);
if (strcasecmp($ext, 'p12') === 0 || strcasecmp($ext, 'pfx') === 0) {
$isP12File = true;
}
} else {
if (!is_object($this->responseMlePrivateKey) || get_class($this->responseMlePrivateKey) !== 'OpenSSLAsymmetricKey') {
$error_message = "Response MLE private key object is invalid. Expected OpenSSLAsymmetricKey";
Expand All @@ -1778,6 +1774,14 @@ private function validateMLEConfiguration(){
throw $exception;
}
}

// Validate responseMleKID
if (!$isP12File && empty(trim($this->responseMleKID))) {
$error_message = "Response MLE enabled but responseMleKID is not set.";
$exception = new AuthException($error_message, 0);
self::$logger->error($error_message);
throw $exception;
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions lib/Authentication/Jwt/JsonWebTokenGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use CyberSource\Authentication\Jwt\JsonWebTokenHeader as JsonWebTokenHeader;
use CyberSource\Authentication\Util\GlobalParameter as GlobalParameter;
use CyberSource\Logging\LogFactory as LogFactory;
use CyberSource\Authentication\Util\MLEUtility as MLEUtility;

//calling the interface
class JsonWebTokenGenerator implements TokenGenerator
Expand All @@ -33,7 +34,7 @@ public function generateToken($resourcePath, $payloadData, $method, $merchantCon
{
$jwtBody = array("iat"=>$date);
if (!empty($isResponseMLEForAPI)) {
$jwtBody['v-c-response-mle-kid'] = $merchantConfig->getResponseMleKID();
$jwtBody['v-c-response-mle-kid'] = MLEUtility::validateAndAutoExtractResponseMleKid($merchantConfig);
}
}
else if($method==GlobalParameter::POST || $method==GlobalParameter::PUT || $method==GlobalParameter::PATCH)
Expand All @@ -42,7 +43,7 @@ public function generateToken($resourcePath, $payloadData, $method, $merchantCon
$digest = $digestObj->generateDigest($payloadData);
$jwtBody = array("digest"=>$digest,"digestAlgorithm"=>"SHA-256","iat"=>$date);
if (!empty($isResponseMLEForAPI)) {
$jwtBody['v-c-response-mle-kid'] = $merchantConfig ->getResponseMleKID();
$jwtBody['v-c-response-mle-kid'] = MLEUtility::validateAndAutoExtractResponseMleKid($merchantConfig);
}
}
else
Expand Down
104 changes: 104 additions & 0 deletions lib/Authentication/Util/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,108 @@ public function getMleResponsePrivateKeyFromFilePath($merchantConfig)
return self::$file_cache[$cacheKey]['response_mle_private_key'] ?? null;

}

/**
* Get MLE KID data from cache
*
* @param MerchantConfiguration $merchantConfig The merchant configuration
* @return array|null Array with 'kid' and 'file_mod_time' keys, or null if not available
*/
public function getMLEKIdDataFromCache($merchantConfig)
{
$filePath = $merchantConfig->getResponseMlePrivateKeyFilePath();
$cacheKey = $filePath . GlobalParameter::RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER;

if (!isset(self::$file_cache[$cacheKey])) {
$this->setUpMLEKIdCache($merchantConfig);
} else {
$cachedMLEKId = self::$file_cache[$cacheKey];
$currentModTime = file_exists($filePath) ? filemtime($filePath) : 0;

if ($cachedMLEKId === null ||
!isset($cachedMLEKId['file_mod_time']) ||
$cachedMLEKId['file_mod_time'] !== $currentModTime) {
self::$logger->info("MLE KID cache outdated or file modified. Refreshing cache for: {$filePath}");
$this->setUpMLEKIdCache($merchantConfig);
}
}

return self::$file_cache[$cacheKey] ?? null;
}

/**
* Set up MLE KID cache
*
* @param MerchantConfiguration $merchantConfig The merchant configuration
* @return void
*/
private function setUpMLEKIdCache($merchantConfig)
{
$filePath = null;
$cacheKey = null;

try {
$filePath = $merchantConfig->getResponseMlePrivateKeyFilePath();
$cacheKey = $filePath . GlobalParameter::RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER;

// Get certificate from P12 file using merchant ID as alias
$merchantCert = Utility::getCertificateByAliasFromPKCS(
$filePath,
$merchantConfig->getResponseMlePrivateKeyFilePassword(),
$merchantConfig->getMerchantID()
);

if ($merchantCert === null) {
throw new MLEException(
"No certificate found for Response MLE Private Key file with merchant ID alias " .
$merchantConfig->getMerchantID()
);
}

// Check if this is a CyberSource-generated P12 file
$isCyberSourceP12 = Utility::isP12GeneratedByCyberSource(
$filePath,
$merchantConfig->getResponseMlePrivateKeyFilePassword()
);

$cachedMLEKId = [
'kid' => null,
'file_mod_time' => file_exists($filePath) ? filemtime($filePath) : 0
];

if ($isCyberSourceP12) {
try {
$mleKID = MLEUtility::extractSerialNumber(
$merchantCert,
"Serial number not found in certificate subject field for Response MLE Private Key with merchant ID alias " .
$merchantConfig->getMerchantID()
);
$cachedMLEKId['kid'] = $mleKID;
} catch (\Exception $e) {
throw new MLEException(
"Failed to extract serial number from certificate for Response MLE: " . $e->getMessage(),
0,
$e
);
}
}

self::$file_cache[$cacheKey] = $cachedMLEKId;

} catch (\Exception $e) {
self::$logger->error(
"Failed to load MLE KID from Response MLE Private Key file: {$filePath}. Error: " .
$e->getMessage()
);

// Store failure marker in cache
if ($cacheKey !== null) {
$failureMarker = [
'kid' => null,
'file_mod_time' => 0
];
self::$file_cache[$cacheKey] = $failureMarker;
}
}
}
}
1 change: 1 addition & 0 deletions lib/Authentication/Util/GlobalParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class GlobalParameter
const MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT = "_mleCertFromMerchantConfig";
const MLE_CACHE_IDENTIFIER_FOR_P12_CERT = "_mleCertFromP12";
const MLE_CACHE_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY = "_mleResponsePrivateKey";
const RESPONSE_MLE_P12_PFX_CACHE_IDENTIFIER = "_responseMleKidFromP12";

const MERCHANT_SECRET_KEY_REQ = " MerchantSecretKey is Mandatory\n";
const KEY_PASSWORD_EMPTY = "KeyPassword Empty/Null. Assigining merchantID value\n";
Expand Down
86 changes: 75 additions & 11 deletions lib/Authentication/Util/MLEUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ public static function decryptMleResponsePayload($merchantConfig, $mleResponseBo
private static function generateToken($cert, $requestBody)
{
try {
$serialNumber = self::extractSerialNumber($cert);
$serialNumber = self::extractSerialNumber(
$cert,
"Serial number not found in certificate subject field for Request MLE encryption"
);

$publicKey = openssl_pkey_get_details(openssl_pkey_get_public($cert))['key'];

Expand Down Expand Up @@ -271,27 +274,88 @@ public static function getMLECert($merchantConfig)
}
}

public static function extractSerialNumber($cert)
public static function extractSerialNumber($cert, $errorMessage = null)
{
try {
$certDetails = openssl_x509_parse($cert);

$serialNumber = null;
if (isset($certDetails['subject']['serialNumber'])) {
$serialNumber = $certDetails['subject']['serialNumber'];
return $certDetails['subject']['serialNumber'];
}

if ($serialNumber === null) {
self::$logger->warning("Serial number not found in MLE certificate for alias.");
$serialNumber = $certDetails['serialNumber'];
}
return $serialNumber;
$errorMsg = $errorMessage ?? "Serial number not found in certificate subject field.";
self::$logger->error($errorMsg);
throw new MLEException($errorMsg);

} catch (MLEException $e) {
throw $e;
} catch (\Exception $e) {
self::$logger->error("Error extracting serial number from certificate: " . $e->getMessage());
throw new MLEException("Error extracting serial number from certificate: " . $e->getMessage());
}
}


/**
* Validates and auto-extracts the Response MLE KID from merchant configuration
*
* This function checks if the Response MLE KID is already configured. If not,
* it attempts to extract it from the Response MLE private key file (P12/PFX).
*
* @param \CyberSource\Authentication\Core\MerchantConfiguration $merchantConfig The merchant configuration
* @return string The validated or extracted Response MLE KID
* @throws MLEException If KID cannot be determined or extraction fails
*/
public static function validateAndAutoExtractResponseMleKid($merchantConfig)
{
if (self::$logger === null) {
self::$logger = (new LogFactory())->getLogger(\CyberSource\Utilities\Helpers\ClassHelper::getClassName(static::class), $merchantConfig->getLogConfiguration());
}

// If responseMlePrivateKey is provided directly (in-memory), use the configured responseMleKID
if ($merchantConfig->getResponseMlePrivateKey() !== null) {
self::$logger->debug("responseMlePrivateKey is provided directly, using configured responseMleKID");
return $merchantConfig->getResponseMleKID();
}

// Attempt to auto-extract KID from Response MLE private key file (CyberSource P12)
$cybsKid = null;
try {
if (!isset(self::$cache)) {
self::$cache = new Cache();
}

$kidData = self::$cache->getMLEKIdDataFromCache($merchantConfig);

if ($kidData !== null && isset($kidData['kid']) && !empty($kidData['kid'])) {
$cybsKid = $kidData['kid'];
self::$logger->debug("Successfully auto-extracted responseMleKID from CyberSource P12 certificate");
}
} catch (\Exception $e) {
self::$logger->debug("Could not auto-extract responseMleKID from certificate: " . $e->getMessage());
}

// Get manually configured KID
$configuredKID = $merchantConfig->getResponseMleKID();
$configuredKID = (is_string($configuredKID) && trim($configuredKID) !== '') ? trim($configuredKID) : null;

// Validate and determine which KID to use
if ($cybsKid === null && $configuredKID === null) {
throw new MLEException(
"responseMleKID is required when response MLE is enabled. " .
"Could not auto-extract from certificate and no manual configuration provided. " .
"Please provide responseMleKID explicitly in your configuration."
);
} else if ($cybsKid === null) {
self::$logger->debug("Using manually configured responseMleKID");
return $configuredKID;
} else if ($configuredKID === null) {
self::$logger->debug("Using auto-extracted responseMleKID from CyberSource certificate");
return $cybsKid;
} else if ($cybsKid !== $configuredKID) {
self::$logger->warning("Auto-extracted responseMleKID does not match manually configured responseMleKID. Using configured value as preference");
}

return $configuredKID;
}
}
?>
?>
69 changes: 69 additions & 0 deletions lib/Authentication/Util/Utility.php
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,73 @@ public static function convertKeyObjectToPem($key): string

return $pemString;
}

/**
* Get certificate by alias from PKCS#12 file
*
* @param string $filePath Path to the PKCS#12 file
* @param string $password Password for the PKCS#12 file
* @param string $alias Alias/CN to search for
* @return string|null Certificate PEM string or null if not found
* @throws \Exception If file cannot be read or parsed
*/
public static function getCertificateByAliasFromPKCS(string $filePath, string $password, string $alias)
{
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new \Exception("PKCS#12 file not found or not readable: {$filePath}");
}

$pkcs12 = file_get_contents($filePath);
if ($pkcs12 === false) {
throw new \Exception("Unable to read PKCS#12 file: {$filePath}");
}

$certs = [];
if (!openssl_pkcs12_read($pkcs12, $certs, $password)) {
$err = openssl_error_string();
throw new \Exception("Unable to parse PKCS#12: {$filePath}. OpenSSL: {$err}");
}

// Try to find certificate by alias using existing method
try {
return self::findCertByAlias($certs, $alias);
} catch (\Exception $e) {
return null;
}
}

/**
* Check if P12 file was generated by CyberSource
*
* @param string $filePath Path to the PKCS#12 file
* @param string $password Password for the PKCS#12 file
* @return bool True if generated by CyberSource, false otherwise
*/
public static function isP12GeneratedByCyberSource(string $filePath, string $password): bool
{
try {
if (!file_exists($filePath) || !is_readable($filePath)) {
return false;
}

$pkcs12 = file_get_contents($filePath);
if ($pkcs12 === false) {
return false;
}

$certs = [];
if (!openssl_pkcs12_read($pkcs12, $certs, $password)) {
return false;
}
try {
$cert = self::findCertByAlias($certs, GlobalParameter::DEFAULT_MLE_ALIAS_FOR_CERT);
return $cert !== null;
} catch (\Exception $e) {
return false;
}
} catch (\Exception $e) {
return false;
}
}

}