In this “How-to” guide, we will show you how to test MLE (Message Level Encryption) enabled APIs using C#.
Important Links:
Encryption Guide https ://developer.visa.com/pages/encryption_guide
Working with Two Way SSL https://developer.visa.com/pages/working-with-visa-apis/two-way-ssl
Login to your Visa Developer Dashboard and go to your project, you should see something like this:
Enable the APIs for which MLE needs to be active in VDP by toggling the API for which MLE needs to be enforced.
In this example, we will enable MLE for Funds Transfer API And Query API as below:
You can obtain your project credentials by browsing the left side navigation menu of your project and click on “Credentials”.
Next step we will create a Key-ID by clicking on the Generate Key-ID button.
After you have clicked the button, you will get Key-ID. Copy the Key-ID for your reference.
The Key-ID will look like this: 41d9f2a1-xxxx-4xxx-b40c-a0480c2xxxxx
The next step is to add a CSR (Certificate Signing Request). Click on the link "Add CSR" .
You will be prompt to submit a Certificate Signing Request.
We have option to Generate a CSR for me (default) or submit your own. In this example we will use the Generate a CSR for me (default) and Click Confirm button.
After submitting the request, you will be prompt to download the Certificate/Copy Private Key.
After you have downloaded the private key, check the box "I confirm that I've downloaded my certificate key" and click continue. You will see the Status change to "Active".
Expand the Key-ID and you will see the Server Encryption Certificate and Client Encryption Certificate.
Download both certificates and save it.
To be able to make an API call with MLE, you need to have the following
On the Visual Studio Welcome Screen, click Create a new Project.
You will need to add the following dependencies using NuGet - the package manager for .NET.
To convert PEM certificate to a PKCS12 certificate, we will use Open SSL.
Execute the following OpenSSL command to create a PKCS12 (.p12) file:
openssl pkcs12 -export -inkey cert.pem -in cert.pem -out cert.p12
Once you have copied the below code, please set the below parameters:
public static string userId = "<YOUR USER ID HERE>";
public static string password = "<YOUR PASSWORD HERE>";
public static string cert = "<ABSOLUTE PATH TO .P12 CERTIFICATE HERE>";
public static string certPassword = "<PASSPHRASE FOR P12 CERTIFICATE HERE>";
//For MLE
public static string keyId = "<YOUR MLE KID HERE>";
public static string mleClientPrivateKey = "<YOUR MLE PRIVATE KEY PATH>";
public static string mleServerPublicCertificate = "<YOUR MLE SERVER CERTIFICATE HERE>";
/*
* (c) Copyright 2018 - 2020 Visa.All Rights Reserved.**
*
* NOTICE: The software and accompanying information and documentation(together, the "Software") remain the property of and are proprietary to Visa and its suppliers and affiliates.The Software remains protected by intellectual property rights and may be covered by U.S.and foreign patents or patent applications.The Software is licensed and not sold.*
*
* By accessing the Software you are agreeing to Visa's terms of use (developer.visa.com/terms) and privacy policy (developer.visa.com/privacy).In addition, all permissible uses of the Software must be in support of Visa products, programs and services provided through the Visa Developer Program (VDP) platform only (developer.visa.com). **THE SOFTWARE AND ANY ASSOCIATED INFORMATION OR DOCUMENTATION IS PROVIDED ON AN "AS IS," "AS AVAILABLE," "WITH ALL FAULTS" BASIS WITHOUT WARRANTY OR CONDITION OF ANY KIND. YOUR USE IS AT YOUR OWN RISK.** All brand names are the property of their respective owners, used for identification purposes only, and do not imply product endorsement or affiliation with Visa. Any links to third party sites are for your information only and equally do not constitute a Visa endorsement. Visa has no insight into and control over third party content and code and disclaims all liability for any such components, including continued availability and functionality. Benefits depend on implementation details and business factors and coding steps shown are exemplary only and do not reflect all necessary elements for the described capabilities. Capabilities and features are subject to Visa's terms and conditions and may require development,implementation and resources by you based on your business and operational details. Please refer to the specific API documentation for details on the requirements, eligibility and geographic availability.*
*
* This Software includes programs, concepts and details under continuing development by Visa. Any Visa features, functionality, implementation, branding, and schedules may be amended, updated or canceled at Visa"s discretion.The timing of widespread availability of programs and functionality is also subject to a number of factors outside Visa's control, including but not limited to deployment of necessary infrastructure by issuers, acquirers, merchants and mobile device manufacturers.*
*
*/
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Jose;
using Org.BouncyCastle.OpenSsl;
using Newtonsoft.Json;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Newtonsoft.Json.Linq;
namespace Vdp
{
class Program
{
public static string visaUrl = "https://sandbox.api.visa.com/";
public static string userId = "<YOUR USER ID HERE>";
public static string password = "<YOUR PASSWORD HERE>";
public static string cert = "<ABSOLUTE PATH TO .P12 CERTIFICATE HERE>";
public static string certPassword = "<PASSPHRASE FOR P12 CERTIFICATE HERE>";
//For MLE
public static string keyId = "<YOUR MLE KID HERE>";
public static string mleClientPrivateKey = "<YOUR MLE PRIVATE KEY PATH>";
public static string mleServerPublicCertificate = "<YOUR MLE SERVER CERTIFICATE HERE>";
static void Main(string[] args)
{
Program p = new Program();
Console.WriteLine("MLE OCT Test");
string decryptedPayload = p.PushFundsTransactions();
Console.WriteLine("Decrypted OCT Response\n" + decryptedPayload);
var responseObj = JObject.Parse(decryptedPayload) as JToken;
var aquiringBin = "408999";
var transactionIdentifier = responseObj["transactionIdentifier"].ToString();
var queryResponse= p.Query(aquiringBin, transactionIdentifier);
Console.WriteLine("Transaction Query Response:\n" + queryResponse);
}
private void LogRequest(string url, string requestBody)
{
Console.WriteLine(url);
Console.WriteLine(requestBody);
}
private void LogResponse(string info, HttpWebResponse response)
{
Debug.WriteLine(info);
Console.WriteLine("Response Status: \n" + response.StatusCode);
Console.WriteLine("Response Headers: \n" + response.Headers.ToString());
Console.WriteLine("Response Body: \n" + GetResponseBody(response));
}
private string GetResponseBody(HttpWebResponse response)
{
string responseBody = "";
using (var reader = new StreamReader(response.GetResponseStream(), ASCIIEncoding.ASCII))
{
responseBody = reader.ReadToEnd();
}
return responseBody;
}
//Correlation Id ( ex-correlation-id ) is an optional header while making an API call. You can skip passing the header while calling the API's.
private string GetCorrelationId()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
return new string(Enumerable.Repeat(chars, 12).Select(s => s[random.Next(s.Length)]).ToArray()) + "_SC";
}
private string GetBasicAuthHeader(string userId, string password)
{
string authString = userId + ":" + password;
var authStringBytes = Encoding.UTF8.GetBytes(authString);
string authHeaderString = Convert.ToBase64String(authStringBytes);
return "Basic " + authHeaderString;
}
public string DoMutualAuthCall(string path, string method, string testInfo, string requestBodyString, Dictionary<string, string> headers = null)
{
string requestURL = visaUrl + path;
string certificatePath = cert;
string certificatePassword = certPassword;
string statusCode = "";
string responseBody = "";
LogRequest(requestURL, requestBodyString);
// Create the POST request object
HttpWebRequest request = WebRequest.Create(requestURL) as HttpWebRequest;
request.Method = method;
if (method.Equals("POST") || method.Equals("PUT"))
{
request.ContentType = "application/json";
request.Accept = "application/json";
// Load the body for the post request
var requestStringBytes = Encoding.UTF8.GetBytes(requestBodyString);
request.GetRequestStream().Write(requestStringBytes, 0, requestStringBytes.Length);
}
if (headers != null)
{
foreach (KeyValuePair<string, string> header in headers)
{
request.Headers[header.Key] = header.Value;
}
}
// Add headers
request.Headers["Authorization"] = GetBasicAuthHeader(userId, password);
request.Headers["ex-correlation-id"] = GetCorrelationId();
request.Headers["keyId"] = keyId;
// Add certificate
var certificate = new X509Certificate2(certificatePath, certificatePassword);
request.ClientCertificates.Add(certificate);
try
{
// Make the call
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
responseBody = GetResponseBody(response);
LogResponse(testInfo, response);
statusCode = response.StatusCode.ToString();
}
}
catch (WebException e)
{
Console.WriteLine(e.ToString());
if (e.Response is HttpWebResponse)
{
HttpWebResponse response = (HttpWebResponse)e.Response;
responseBody = GetResponseBody(response);
LogResponse(testInfo, response);
statusCode = response.StatusCode.ToString();
}
}
return responseBody;
}
public string PushFundsTransactions()
{
string localTransactionDateTime = DateTime.Now.ToString("yyyy-MM-dd'T'HH:mm:ss");
string requestBody = "{ \"acquirerCountryCode\": \"840\", \"acquiringBin\": \"408999\", \"amount\": \"124.05\", \"businessApplicationId\": \"AA\", \"cardAcceptor\": { \"address\": { \"country\": \"USA\", \"county\": \"San Mateo\", \"state\": \"CA\", \"zipCode\": \"94404\" }, \"idCode\": \"CA-IDCode-77765\", \"name\": \"Visa Inc. USA-Foster City\", \"terminalId\": \"TID-9999\" }, \"localTransactionDateTime\": \"" + localTransactionDateTime + "\", \"merchantCategoryCode\": \"6012\", \"pointOfServiceData\": { \"motoECIIndicator\": \"0\", \"panEntryMode\": \"90\", \"posConditionCode\": \"00\" }, \"recipientName\": \"rohan\", \"recipientPrimaryAccountNumber\": \"4957030420210462\", \"retrievalReferenceNumber\": \"412770451018\", \"senderAccountNumber\": \"4957030420210454\", \"senderAddress\": \"901 Metro Center Blvd\", \"senderCity\": \"Foster City\", \"senderCountryCode\": \"124\", \"senderName\": \"Mohammed Qasim\", \"senderReference\": \"\", \"senderStateCode\": \"CA\", \"sourceOfFundsCode\": \"05\", \"systemsTraceAuditNumber\": \"451018\", \"transactionCurrencyCode\": \"USD\", \"transactionIdentifier\": \"381228649430015\" }";
string requestURL = "visadirect/fundstransfer/v1/pushfundstransactions";
return GetDecryptedPayload(DoMutualAuthCall(requestURL, "POST", "OCT With MLE", getEncryptedPayload(requestBody), null));
}
public string Query(string acquiringBin, string transactionIdentifier)
{
var queryString = "?acquiringBIN=" + acquiringBin + "&transactionIdentifier=" + transactionIdentifier;
var requestUrl = "visadirect/v1/transactionquery" + queryString;
return GetDecryptedPayload(DoMutualAuthCall(requestUrl, "GET", "Transaction Query With MLE", null, null));
}
private static string GetTimestamp()
{
long timeStamp = ((long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds) / 1000;
return timeStamp.ToString();
}
private String getEncryptedPayload(String requestBody)
{
RSA clientCertificate = new X509Certificate2(mleServerPublicCertificate).GetRSAPublicKey();
DateTime now = DateTime.UtcNow;
long unixTimeMilliseconds = new DateTimeOffset(now).ToUnixTimeMilliseconds();
IDictionary<string, object> extraHeaders = new Dictionary<string, object>{
{"kid", keyId},{"iat",unixTimeMilliseconds}
};
string token = JWT.Encode(requestBody, clientCertificate, JweAlgorithm.RSA_OAEP_256, JweEncryption.A128GCM, null, extraHeaders);
return "{\"encData\":\"" + token + "\"}";
}
private static String GetDecryptedPayload(String encryptedPayload)
{
var jsonPayload = JsonConvert.DeserializeObject<EncryptedPayload>(encryptedPayload);
return JWT.Decode(jsonPayload.encData, ImportPrivateKey(mleClientPrivateKey));
}
private static RSA ImportPrivateKey(string privateKeyFile)
{
var pemValue = System.Text.Encoding.Default.GetString(File.ReadAllBytes(privateKeyFile));
var pr = new PemReader(new StringReader(pemValue));
var keyPair = (AsymmetricCipherKeyPair)pr.ReadObject();
var rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)keyPair.Private);
var rsa = RSA.Create();
rsa.ImportParameters(rsaParams);
return rsa;
}
}
public class EncryptedPayload
{
public string encData { get; set; }
}
}
Want more? Join the Visa Developer Community to get alerts on the latest tutorials, guides and new developer resources. Stay tuned for more in the series.
Hi @locdh31439 ,
.NET does not have an easy way to directly deal with .pem format files generated using OpenSSL. I had to look into Bouncy Castle library to do it.
The DotNetUtilities class from BouncyCastle provides helper methods. In this sample code, I have used theDotNetUtilities.ToRSAParameters(...) to get the RSAParameters to be able to construct the RSA object, from the private key which is in PEM format.
Please note, the sample code is exemplary only to demonstrate how MLE works.
Thank you.
Shameem
Was your question answered? Don't forget to click on "Accept as Solution".
In Step 3 - Copy the below code to the “Program.cs” file
>To convert PEM certificate to a PKCS12 certificate, we will use Open SSL.
>Execute the following OpenSSL command to create a PKCS12 (.p12) file:
>openssl pkcs12 -export -inkey cert.pem -in cert.pem -out cert.p12
Running that on the client and server certificates for the key ID of the project I get the following for both:
C:\Projects\Visa\Transactions>openssl pkcs12 -export -inkey client_cert_0ece2225-470c-4445-8614-ea9fb9e38b3b.pem -in client_cert_0ece2225-470c-4445-8614-ea9fb9e38b3b.pem -out client_cert.p12
unable to load private key
26384:error:0906D06C:PEM routines:PEM_read_bio:no start line:pem_lib.c:707:Expecting: ANY PRIVATE KEY
when I try and run this it seems to make the call to the server, but when opening the reply it runs into an error when it tries to decrypt the payload.
It works OK when I use the same cert with Soap UI - though the API called in that example is visaaliasdirectory/v1/manage/createalias
I tried
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pr.ReadObject();
instead of
var keyPair = (AsymmetricCipherKeyPair)pr.ReadObject();
but it did not help
System.InvalidCastException
HResult=0x80004002
Message=Unable to cast object of type 'Org.BouncyCastle.X509.X509Certificate' to type 'Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair'.
Source=VisaMLENet
StackTrace:
at VisaMLENet.Program.ImportPrivateKey(String privateKeyFile) in
Hello @asaldanha ,
Thank you for trying out our sample codes. Kindly find comments below:
1. unable to load private key
The -inkey option should be the private key not the certificate or public key.
Both inkey and in options you are providing the same certificate. Hope you have managed to resolve this.
C:\Projects\Visa\Transactions>openssl pkcs12 -export -inkey client_cert_0ece2225-470c-4445-8614-ea9fb9e38b3b.pem -in client_cert_0ece2225-470c-4445-8614-ea9fb9e38b3b.pem -out client_cert.p12
unable to load private key
2. Unable to cast object of type 'Org.BouncyCastle.X509.X509Certificate' to type 'Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair'.
Looks like you are using the wrong import or the wrong library. Below is the project dependencies I am using:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="jose-jwt" Version="3.1.1" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.11.0" />
</ItemGroup>
</Project>
Here are the required imports:
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Jose;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
Let us know if you have any further questions.
Thank you.
This is WRT 2.
Does console application have to be in .Net 5 ?
Mine is 4.8 and your code needs more imports that you specified to compile for 4.8
My libraries are the same as yours (below)
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Jose;
using Org.BouncyCastle.OpenSsl;
using Newtonsoft.Json;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Newtonsoft.Json.Linq;
Have not heard back on this.
I have re-built another project in .Net 5 - but am getting the same run time error as earlier.
Would appreciate feedback
Hi @asaldanha ,
Looks like you are not using the correct file to be loaded as the mle private key. Consequently causing a casting exception.
By looking at the error message, looks like you are provide a public certificate not a private key.
Message=Unable to cast object of type 'Org.BouncyCastle.X509.X509Certificate' to type 'Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair'.
Kindly ensure that the mle private key is set to avoid this error, The certificates required for MLE configuration contains your MLE Key ID as below:
public static string keyId = "<MLE_KEY_ID>";
public static string mleClientPrivateKey = "<PATH>/server_cert_<MLE_KEY_ID>.pem";
public static string mleServerPublicCertificate = "<PATH>/key_<MLE_KEY_ID>.pem";
That should solve this issue.
Let us know if you have any further questions.
Thank you.