Note: If your RFI response includes a file, that file is encrypted in a different manner.
This guide explains how to set up encryption and decryption between an OFI and a BFI so that messages can be passed securely during payment flow. CPN uses JSON Web Encryption (JWE) for encrypting data in a secure and compact manner. This encryption is implemented after a quote is accepted: the JWK data and public key for encryption are shared in the quote response. The encryption is also used when communicating RFI data.
Both OFI and BFI can implement this encryption system in any programming language that supports the required primitives. This encryption scheme is required when performing the following tasks in CPN:
Note: If your RFI response includes a file, that file is encrypted in a different manner.
Regardless of the language used to implement the encryption, the following parameters must be followed:
The following sections describe the steps necessary to encrypt a message sent from an OFI to a BFI. Note that this example uses beneficiary account data and travel rule data, but the same encryption scheme applies for RFI data as well.
Quote responses include a certificate field with required parameters to establish encryption with the BFI. A sample (truncated) quote response is below:
{
"data": [
{
"id": "6e4c7e85-39eb-4411-8c4a-683ff73846d6",
"paymentMethodType": "FPS",
"blockchain": "MATIC-AMOY",
"senderCountry": "US",
"destinationCountry": "HK",
"certificate": {
"id": "0cc3c5fe-fa88-4e79-b5eb-1c5194a19b08",
"certPem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tTUlJQmpUQ0NBVE9nQXdJQkFnSVVMaWk2Mk5KME0rdTZOTDZWV0hWRkhIZmJCWUl3Q2dZSUtvWkl6ajBFQXdJd0tqRVhNQlVHQTFVRUF3d09ZWEJwTG1OcGNtTnNaUzVqYjIweER6QU5CZ05WQkFvTUJrTnBjbU5zWlRBZUZ3MHlOVEF6TVRjeU1EQXdNVFJhRncweU5qQXpNVGN5TURBd01UUmFNQ294RnpBVkJnTlZCQU1NRG1Gd2FTNWphWEpqYkdVdVkyOXRNUTh3RFFZRFZRUUtEQVpEYVhKamJHVXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUmgyTTU0Q2FVMTlaWFRFaXZJVUNLOXluMmgvYld6Uno0bUhJWVE0ZzFYWnArdHRiM3Z6bGY2ZDQzYUhNYlRaQUpPTG1pbkdFZGwxbUZMdFRUTXdYb3ZvemN3TlRBekJnTlZIUkVFTERBcWdnNWhjR2t1WTJseVkyeGxMbU52YllJU2QzZDNMbUZ3YVM1amFYSmpiR1V1WTI5dGh3UUtBQUFCTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUExbksrNUxBUC9ueUlxRFlUaVVLYmlHNWYwTjVPUmFMb2Y1VXpXU0dsUEJBaUVBaEVOcDFxakRydG41aGFpMHdKeTNORzJKZ2xra084Y1QzellhN21mRTBiST0tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t",
"domain": "api.circle.com",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "263521881931753643998528753619816524468853605762",
"x": "YdjOeAmlNfWV0xIryFAivcp9of21s0c-JhyGEOINV2Y",
"y": "n621ve_OV_p3jdocxtNkAk4uaKcYR2XWYUu1NMzBei8"
}
}
}
]
}
From the certificate
object in the quote response, extract the base64
encoded format of the certificate from the certPem
field, decode it, and
verify the following:
domain
field in the responsejwk
field in the
responsepublic static void verify() throws Exception {
// Assume the API response is stored in a file "response.json"
String responseStr = new String(Files.readAllBytes(Paths.get("response.json")));
JSONObject response = new JSONObject(responseStr);
JSONObject certObj = response.getJSONArray("data").getJSONObject(0).getJSONObject("certificate");
String caBundlePath = "ca_bundle.pem"; // CA bundle file provided by Circle
// ---------- 1. Check certificate expiration ----------
String certPemB64 = certObj.getString("certPem");
byte[] pemData = Base64.getDecoder().decode(certPemB64);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream in = new ByteArrayInputStream(pemData);
X509Certificate certificate = (X509Certificate) cf.generateCertificate(in);
Date expirationDate = certificate.getNotAfter();
Date currentDate = new Date();
if (currentDate.after(expirationDate)) {
System.out.println("❌ Certificate has expired on " + expirationDate);
System.exit(1);
} else {
System.out.println("✅ Certificate is valid until " + expirationDate);
}
// ---------- 2. Verify that the certificate was signed by a Circle approved CA ----------
List<X509Certificate> caCerts = loadCABundle(caBundlePath);
boolean trusted = false;
for (X509Certificate caCert : caCerts) {
if (certificate.getIssuerX500Principal().equals(caCert.getSubjectX500Principal())) {
trusted = true;
System.out.println("✅ Certificate is signed by trusted CA: " + caCert.getSubjectX500Principal());
break;
}
}
if (!trusted) {
System.out.println("❌ Certificate verification failed. CA is not trusted.");
System.exit(1);
}
// ---------- 3. Verify that the certificate's common name (CN) matches the expected domain ----------
String certCN = getCommonName(certificate);
String expectedDomain = certObj.getString("domain");
if (certCN != null && certCN.equals(expectedDomain)) {
System.out.println("✅ Certificate common name matches expected domain.");
} else {
System.out.println("❌ Certificate common name does not match expected domain.");
System.exit(1);
}
// ---------- 4. Verify that the verifiable public key of the certificate is the same as the provided JWK ----------
JSONObject providedJWK = certObj.getJSONObject("jwk");
String xProvided = providedJWK.getString("x");
String yProvided = providedJWK.getString("y");
PublicKey pubKey = certificate.getPublicKey();
if (pubKey instanceof ECPublicKey) {
ECPublicKey ecPub = (ECPublicKey) pubKey;
ECPoint point = ecPub.getW();
String xCert = toBase64URL(point.getAffineX(), 32);
String yCert = toBase64URL(point.getAffineY(), 32);
if (xCert.equals(xProvided) && yCert.equals(yProvided)) {
System.out.println("✅ Certificate public key matches provided JWK.");
} else {
System.out.println("❌ Certificate public key does not match provided JWK.");
System.exit(1);
}
} else {
System.out.println("❌ Certificate public key is not EC type.");
System.exit(1);
}
}
From the certificate
field, extract the JWK parameters: kty
, crv
, kid
,
x
, y
and create the JWK object in your code using a suitable library (for
example, Nimbus JOSE+JWT in Java).
import com.nimbusds.jose.jwk.JWK;
public static JWK parseJwkFromJson() throws ParseException {
String jwkJson = """
{
"kty": "EC",
"crv": "P-256",
"kid": "263521881931753643998528753619816524468853605762",
"x": "YdjOeAmlNfWV0xIryFAivcp9of21s0c-JhyGEOINV2Y",
"y": "n621ve_OV_p3jdocxtNkAk4uaKcYR2XWYUu1NMzBei8"
}
""";
JWK jwk = JWK.parse(jwkJson);
return jwk;
}
Create the payload for travel rule data and beneficiary account data. Follow the steps in How-to: Integrate with JSON Schema to get the required fields from the requirements endpoint and construct them to the correct format. An example of each is shown below:
travelRule
{
"BENEFICIARY_ADDRESS": {
"city": "San Francisco",
"country": "US",
"postalCode": "94105",
"stateProvince": "CA",
"street": "123 Market Street"
},
"BENEFICIARY_NAME": "Alice Johnson",
"ORIGINATOR_ACCOUNT_NUMBER": "9876543210",
"ORIGINATOR_ADDRESS": {
"city": "New York",
"country": "US",
"postalCode": "10001",
"stateProvince": "NY",
"street": "456 Madison Avenue"
},
"ORIGINATOR_FINANCIAL_INSTITUTION_ADDRESS": {
"city": "Chicago",
"country": "US",
"postalCode": "60603",
"stateProvince": "IL",
"street": "789 Apple Drive"
},
"ORIGINATOR_FINANCIAL_INSTITUTION_NAME": "First National Bank",
"ORIGINATOR_NAME": "Robert Smith"
}
beneficiaryAccount
{
"BANK_NAME": "Test Bank",
"RECIPIENT_ADDRESS": {
"street": "123 Test St",
"city": "Sacramento",
"state": "CA"
},
"RECIPIENT_CITY": "Sacramento"
}
Convert the data from the previous step into a JSON string, then use the JWK to encrypt it. Ensure that the following parameters are used:
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEEncrypter;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDHEncrypter;
import com.nimbusds.jose.jwk.JWK;
/**
* Encrypts a payload using JWE (JSON Web Encryption) with ECDH-ES key agreement.
*
* This method creates a JWE compact string with the provided payload and encrypts it using
* the recipient's public key. The encryption algorithm used is ECDH-ES with AES-128 key wrap,
* and the content encryption method is AES GCM with 128-bit key.
*
* @param <T> the type of the payload to encrypt
* @param payload the data to be encrypted
* @param recipientJwk the recipient's JWK (JSON Web Key) containing the public key for encryption
* @return the serialized JWE in compact form
* @throws JOSEException if an error occurs during the encryption process
*/
public static <T> String encrypt(T payload, JWK recipientJwk) throws JOSEException {
String plainText = JsonUtils.toJson(payload);
// Create the JWEHeader using ECDH_ES+AS128KW and AES-128-GCM
JWEHeader header = new JWEHeader(
JWEAlgorithm.ECDH_ES_A128KW,
EncryptionMethod.A128GCM
);
// Create the JWE object with the payload to encrypt
JWEObject jweObject = new JWEObject(
header,
new Payload(plainText)
);
// Create an encrypter with the recipient's public key
JWEEncrypter encrypter = new ECDHEncrypter(recipientJwk.toECKey());
// Encrypt the JWE
jweObject.encrypt(encrypter);
// Return the serialized JWE string in compact form
return new jweObject.serialize();
}
Send the encrypted payload in the API request to
create a payment. The API
returns a 200
response if the data is properly encrypted and can be decrypted
by the BFI, otherwise an
encryption-related error code is
returned.