Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ed8867f
renamed MleKeyAlias to RequestMleKeyAlias
aastgoel Sep 3, 2025
d0ffbf4
Added Response MLE Params in the Merchant Config
aastgoel Sep 10, 2025
7e88865
updated Constructor and set the Merchant Config Params
aastgoel Sep 10, 2025
b84c08e
Added Validation for Response MLE Params
aastgoel Sep 10, 2025
39db3fb
added new constructor and convert function for mapToControlMLEonAPI
aastgoel Sep 11, 2025
9296776
modified setter for mapToControlMLEonAPI and added validation for Map…
aastgoel Sep 11, 2025
8b7321e
modified MLE Validation to work with new split maps and changed the m…
aastgoel Sep 11, 2025
a35a638
added Validation for resonseMleKID config
aastgoel Sep 11, 2025
9dc1b91
added CheckIsResponseMLEForAPI function in MLEUtility
aastgoel Sep 11, 2025
50f0ee1
dropping support for <string,bool> mapToControlMLEonAPI
aastgoel Sep 12, 2025
3fb548d
added doc for RequestMleKeyAlias
aastgoel Sep 17, 2025
5dff2f4
added v-c-response-mle-kid in jwt body if response MLE for API is ena…
aastgoel Sep 17, 2025
71f717d
renamed GetMLECertificate to GetRequestMLECertificate
aastgoel Sep 17, 2025
06136a9
Added validation for responseMlePrivateKey and responseMlePrivateKeyF…
aastgoel Sep 17, 2025
4cb207b
Added function to read private key from files like pem, p12 , p8 etc
aastgoel Sep 18, 2025
9b4505b
new label added for response mle key
aastgoel Sep 18, 2025
9006dc7
added Caching support for Response MLE Private Key
aastgoel Sep 21, 2025
08dce7f
added CheckIsMleEncryptedResponse and DecryptMleResponsePayload in ML…
aastgoel Sep 21, 2025
da3f3d4
changed error messages for private key handling from files
aastgoel Sep 23, 2025
f068c07
fixed log message
aastgoel Sep 23, 2025
57de176
changed the mustache files for API , added isResponseMLEForApi flag a…
aastgoel Sep 23, 2025
cc85fec
changed ApiClient functions to make use of isResponseMLEForAPi flag. …
aastgoel Sep 23, 2025
030cb00
added new param ResponseMlePrivateKey, changed mapToControlMLEonAPI d…
aastgoel Sep 23, 2025
503da26
minor fix
aastgoel Sep 23, 2025
040a2f0
PKCS1 encrypted keys are supported, removed exception
aastgoel Sep 23, 2025
4177403
updated error handling for wrong PrivateKey use
aastgoel Sep 23, 2025
a75d3b0
Merge remote-tracking branch 'origin/restclient-rewrite' into feature…
aastgoel Sep 24, 2025
9f2e255
Added responseMlePrivateKey parameter to MerchantConfig initialization
aastgoel Sep 24, 2025
ecc2786
Merge remote-tracking branch 'origin/restclient-rewrite' into feature…
aastgoel Sep 24, 2025
0ce5409
removed isResponseMLEForApi from JwtToken file, handled it in JwtToke…
aastgoel Sep 25, 2025
1d39df5
replaced string with SecureString for password field
aastgoel Sep 25, 2025
0c10f62
changed OAuthApi deserialize function call
aastgoel Sep 26, 2025
6aa53b2
minor fix
aastgoel Sep 26, 2025
7cd956b
review comment changes
aastgoel Sep 26, 2025
56776ea
review comment changes
aastgoel Sep 30, 2025
5eae8b1
moved Utility Functions to PEMUtility
aastgoel Sep 30, 2025
a8193cf
Merge branch 'feature/mle-for-response-2' into feature/mle-for-respon…
aastgoel Oct 6, 2025
19a30eb
Merge pull request #130 from CyberSource/feature/mle-for-response-2
gaubansa Oct 6, 2025
524dbf5
proper error handling
aastgoel Oct 6, 2025
d43a5fd
Merge pull request #131 from CyberSource/feature/mle-for-response-3
aastgoel Oct 8, 2025
4dff61c
Merge remote-tracking branch 'origin/future' into feature/mle-for-res…
aastgoel Oct 8, 2025
40165ed
fixed checkmarx findings
aastgoel Oct 8, 2025
e9ea9ad
made code same as dotnet
aastgoel Oct 9, 2025
eea5b1b
removed unused line of code
aastgoel Oct 17, 2025
e750a78
masked sensitive information in response MLE logs
aastgoel Oct 17, 2025
ef11971
updated MLE.md
aastgoel Oct 18, 2025
91d8ebf
Merge branch 'future' into feature/mle-for-response
aastgoel Oct 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
using System.Security.Cryptography.X509Certificates;
using System.Text;
using AuthenticationSdk.core;
using Newtonsoft.Json.Linq;

namespace AuthenticationSdk.authentication.jwt
{
public class JwtTokenGenerator : ITokenGenerator
{
private readonly MerchantConfig _merchantConfig;
private readonly JwtToken _jwtToken;
private readonly bool _isResponseMLEForApi;

public JwtTokenGenerator(MerchantConfig merchantConfig)
public JwtTokenGenerator(MerchantConfig merchantConfig, bool isResponseMLEForApi)
{
_isResponseMLEForApi = isResponseMLEForApi;
_merchantConfig = merchantConfig;
_jwtToken = new JwtToken(_merchantConfig);
}
Expand Down Expand Up @@ -54,7 +57,16 @@ private string SetToken()

private string TokenForCategory1()
{
var jwtBody = $"{{ \"iat\":\"{DateTime.Now.ToUniversalTime().ToString("r")}\"}}";
JObject claimSetJson = new JObject();
claimSetJson["iat"] = DateTime.Now.ToUniversalTime().ToString("r");

if (_isResponseMLEForApi)
{
claimSetJson["v-c-response-mle-kid"] = _merchantConfig.ResponseMleKID;
}

String jwtBody = "";
jwtBody = claimSetJson.ToString(Newtonsoft.Json.Formatting.None);

var x5Cert = _jwtToken.Certificate;

Expand Down Expand Up @@ -82,7 +94,18 @@ private string TokenForCategory2()
{
var digest = GenerateDigest(_jwtToken.RequestJsonData);

var jwtBody = $"{{\n \"digest\":\"{digest}\", \"digestAlgorithm\":\"SHA-256\", \"iat\":\"{DateTime.Now.ToUniversalTime().ToString("r")}\"}}";
JObject claimSetJson = new JObject();
claimSetJson["digest"] = digest;
claimSetJson["digestAlgorithm"] = "SHA-256";
claimSetJson["iat"] = DateTime.Now.ToUniversalTime().ToString("r");

if (_isResponseMLEForApi)
{
claimSetJson["v-c-response-mle-kid"] = _merchantConfig.ResponseMleKID;
}

String jwtBody = "";
jwtBody = claimSetJson.ToString(Newtonsoft.Json.Formatting.None);

var x5Cert = _jwtToken.Certificate;

Expand All @@ -101,7 +124,6 @@ private string TokenForCategory2()
};

var token = Jose.JWT.Encode(jwtBody, privateKey, Jose.JwsAlgorithm.RS256, cybsHeaders);

return token;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public HttpToken GetSignature()
* @return a JwtToken object (JWT Bearer Token),
* based on the Merchant Configuration passed to the Constructor of Authorize Class
*/
public JwtToken GetToken()
public JwtToken GetToken(bool isResponseMLEForApi = false)
{
try
{
Expand All @@ -101,7 +101,7 @@ public JwtToken GetToken()
throw new Exception("Missing or Empty Credentials : MerchantID or KeyAlias or KeyPassphrase");
}

var tokenObj = (JwtToken)new JwtTokenGenerator(_merchantConfig).GetToken();
var tokenObj = (JwtToken)new JwtTokenGenerator(_merchantConfig, isResponseMLEForApi).GetToken();

if (_merchantConfig.IsGetRequest || _merchantConfig.IsDeleteRequest)
{
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.Caching;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
Expand All @@ -35,6 +36,12 @@ private class CertInfo
public X509Certificate2 MLECertificate { get; set; }
}

private class PrivateKeyInfo
{
public AsymmetricAlgorithm PrivateKey { get; set; }
public DateTime Timestamp { get; set; }
}

public static X509Certificate2Collection FetchCachedCertificate(string p12FilePath, string keyPassword)
{
try
Expand Down Expand Up @@ -173,6 +180,57 @@ private static X509Certificate2 GetMLECertBasedOnCacheKey(MerchantConfig merchan

private static void SetupCache(MerchantConfig merchantConfig, string cacheKey, string certificateFilePath)
{
var policy = new CacheItemPolicy();
var filePaths = new List<string>();
var cachedFilePath = Path.GetFullPath(certificateFilePath);
filePaths.Add(cachedFilePath);
policy.ChangeMonitors.Add(new HostFileChangeMonitor(filePaths));

ObjectCache cache = MemoryCache.Default;

if (cacheKey.EndsWith(Constants.MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY))
{
try
{
string fileExtension = Path.GetExtension(certificateFilePath)?.TrimStart('.').ToLowerInvariant();
AsymmetricAlgorithm mlePrivateKey = null;
SecureString password = merchantConfig.ResponseMlePrivateKeyFilePassword;

// Case 1 - PKCS#12 formats (.p12, .pfx)
if (fileExtension.Equals("p12") || fileExtension.Equals("pfx"))
{
mlePrivateKey = Utility.ReadPrivateKeyFromP12(certificateFilePath, password);
}
// Case 2 - PEM-based formats (.pem, .key, .p8)
else if (fileExtension.Equals("pem") || fileExtension.Equals("key") || fileExtension.Equals("p8"))
{
mlePrivateKey = (AsymmetricAlgorithm) Utility.ExtractPrivateKeyFromFile(certificateFilePath, password);
}
else
{
throw new Exception($"Unsupported Response MLE Private Key file format: {fileExtension}. Supported formats are: .p12, .pfx, .pem, .key, .p8");
}

PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo
{
PrivateKey = mlePrivateKey,
Timestamp = File.GetLastWriteTime(certificateFilePath)
};

lock (mutex)
{
cache.Set(cacheKey, privateKeyInfo, policy);
}
}
catch (Exception e)
{
logger.Error($"Error loading MLE response private key from: {certificateFilePath}. Error: {e.Message}");
throw new Exception($"Error loading MLE response private key from: {certificateFilePath}. Error: {e.Message}", e);
}
return;
}

// ... existing code for other cacheKey cases ...
X509Certificate2 mleCertificate = null;

if (cacheKey.EndsWith(Constants.MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT))
Expand All @@ -181,15 +239,15 @@ private static void SetupCache(MerchantConfig merchantConfig, string cacheKey, s

try
{
mleCertificate = GetCertBasedOnKeyAlias(certificates, merchantConfig.MleKeyAlias);
mleCertificate = GetCertBasedOnKeyAlias(certificates, merchantConfig.RequestMleKeyAlias);
}
catch (Exception)
{
if (mleCertificate == null)
{
// If no certificate found for the specified alias, fall back to first certificate
string fileName = Path.GetFileName(certificateFilePath);
logger.Warn($"No certificate found for the specified mleKeyAlias '{merchantConfig.MleKeyAlias}'. Using the first certificate from file {fileName} as the MLE request certificate.");
logger.Warn($"No certificate found for the specified requestMleKeyAlias '{merchantConfig.RequestMleKeyAlias}'. Using the first certificate from file {fileName} as the MLE request certificate.");
mleCertificate = certificates[0];
}
}
Expand All @@ -199,13 +257,13 @@ private static void SetupCache(MerchantConfig merchantConfig, string cacheKey, s
{
try
{
mleCertificate = GetCertBasedOnKeyAlias(FetchCertificateCollectionFromP12File(merchantConfig.P12Keyfilepath, merchantConfig.KeyPass), merchantConfig.MleKeyAlias);
mleCertificate = GetCertBasedOnKeyAlias(FetchCertificateCollectionFromP12File(merchantConfig.P12Keyfilepath, merchantConfig.KeyPass), merchantConfig.RequestMleKeyAlias);
}
catch (Exception)
{
string fileName = Path.GetFileName(merchantConfig.P12Keyfilepath);
logger.Error($"No certificate found for the specified mleKeyAlias '{merchantConfig.MleKeyAlias}' in file {fileName}.");
throw new ArgumentException($"No certificate found for the specified mleKeyAlias '{merchantConfig.MleKeyAlias}' in file {fileName}.");
logger.Error($"No certificate found for the specified requestMleKeyAlias '{merchantConfig.RequestMleKeyAlias}' in file {fileName}.");
throw new ArgumentException($"No certificate found for the specified requestMleKeyAlias '{merchantConfig.RequestMleKeyAlias}' in file {fileName}.");
}
}

Expand All @@ -215,13 +273,7 @@ private static void SetupCache(MerchantConfig merchantConfig, string cacheKey, s
Timestamp = File.GetLastWriteTime(certificateFilePath)
};

var policy = new CacheItemPolicy();
var filePaths = new List<string>();
var cachedFilePath = Path.GetFullPath(certificateFilePath);
filePaths.Add(cachedFilePath);
policy.ChangeMonitors.Add(new HostFileChangeMonitor(filePaths));

ObjectCache cache = MemoryCache.Default;
lock(mutex)
{
cache.Set(cacheKey, certInfo, policy);
Expand All @@ -236,5 +288,40 @@ private static X509Certificate2Collection FetchCertificateCollectionFromP12File(
//return all certs in p12
return certificates;
}
public static AsymmetricAlgorithm GetMleResponsePrivateKeyFromFilePath(MerchantConfig merchantConfig)
{
string merchantId = merchantConfig.MerchantId;
string identifier = Constants.MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY;
string cacheKey = $"{merchantId}_{identifier}";
string mleResponsePrivateKeyFilePath = merchantConfig.ResponseMlePrivateKeyFilePath;

ObjectCache cache = MemoryCache.Default;

if (!cache.Contains(cacheKey))
{
SetupCache(merchantConfig, cacheKey, mleResponsePrivateKeyFilePath);
}
else
{
var responseMlePrivateKeyInfo = (PrivateKeyInfo)cache.Get(cacheKey);
if (responseMlePrivateKeyInfo == null || responseMlePrivateKeyInfo.Timestamp != File.GetLastWriteTime(mleResponsePrivateKeyFilePath))
{
SetupCache(merchantConfig, cacheKey, mleResponsePrivateKeyFilePath);
}
}

var cachedResponseMlePrivateKeyInfo = (PrivateKeyInfo)cache.Get(cacheKey);

try
{
RSA privateKey = (RSA)cachedResponseMlePrivateKeyInfo.PrivateKey;
return cachedResponseMlePrivateKeyInfo.PrivateKey;
}
catch (Exception ex)
{
logger.Error($"Error retrieving MLE response private key: {ex.Message}");
throw new Exception($"{Constants.ErrorPrefix} Failed to retrieve MLE response private key from cache.", ex);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,16 @@ public static class Constants
public static readonly string MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT = "mleCertFromMerchantConfig";

public static readonly string MLE_CACHE_IDENTIFIER_FOR_P12_CERT = "mleCertFromP12";

public static readonly string MLE_CACHE_KEY_IDENTIFIER_FOR_RESPONSE_PRIVATE_KEY = "mleResponsePrivateKeyFromFile";

public static readonly string PKCS8_PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";

public static readonly string PKCS8_ENCRYPTED_PRIVATE_KEY_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";

public static readonly string PKCS1_PRIVATE_KEY_HEADER = "-----BEGIN RSA PRIVATE KEY-----";

public static readonly string PROC_TYPE_ENCRYPTED_HEADER = "Proc-Type: 4,ENCRYPTED";

}
}
Loading