Apple Pay Implementation - Decrypt the Apple Pay payload
Frontend
Pass the encrypted token to the backend to be decrypted
Steps
- The Apple Pay SDK creates ApplePayPayment within session.onpaymentauthorized. ApplePayPayment is the result of authenticating a payment request and contains card info, billing info, and shipping info. ApplePayPayment is accessed via event.payment.
The encrypted Apple Pay token to be sent to the backend is accessed via event.payment.token.paymentData.
-
Within session.onpaymentauthorized, create a dictionary called data.
-
Within data, create a key value pair. Make the key encryptedToken. Assign its corresponding value to event.payment.token.paymentData.
Your dictionary should be as follows:const data = { encryptedToken: event.payment.token.paymentData };
-
Create the POST request to pass the token to your backend. Some points to note:
- The body should be a JSON string of your data dictionary.
- You want your response parsed as JSON
-
Your resulting POST request should look something like this:
fetch('https://<FQDN>:1234/decryptToken', { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(res => console.log(res.json())) .catch(err => console.error(err));
Your frontend is complete! The resulting Javascript file should be as follows (note: this code expands on previous example)
//*********************************** // Copyright (c) 2022. TabaPay, Inc. All Rights Reserved. //*********************************** //*********************************** // Checks if Apple Pay JS API is available in browser // Shows button if true, else prompts user to open in Safari //*********************************** if(window.ApplePaySession){ //=================================== // Merchant Identifier should be what is set on Apple Developer website //=================================== let merchantIdentifier = 'yourApplePayMerchantIdentifier'; let promise = ApplePaySession.canMakePaymentsWithActiveCard(merchantIdentifier); promise.then(function (canMakePayments){ if(canMakePayments){ console.log('Apple Pay is supported'); const e = document.getElementById('ape'); e.style.display = 'none'; } }) } else { console.log('Please open on a supported browser'); const e = document.getElementById('ape'); e.style.display = 'block'; } //*********************************** // Function that contacts your server, requests session from AP server, // then returns an opaque merchant session object //*********************************** const validateMerchant = async (validationURL) => { //=================================== // URL to Apple Pay servers //=================================== const data = {validationURL: validationURL}; //----------------------------------- // POST to backend //----------------------------------- const response = await fetch('yourAPIEndpoint', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Return //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ return response.json(); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ }; //*********************************** // Event listener for when Apple Pay button is clicked //*********************************** const onApplePayButtonClicked = () => { //=================================== // Customizations //=================================== const request = { "countryCode": "US", "currencyCode": "USD", "merchantCapabilities": [ "supports3DS" ], "supportedNetworks": [ "visa", "masterCard", "amex", "discover" ], //----------------------------------- // Customizing touch bar //----------------------------------- "total": { "label": "Demo (Card is not charged)", "type": "final", "amount": "1.99" } }; //=================================== // Create ApplePaySDK instance //=================================== const session = new ApplePaySession(3, request); //=================================== // As soon as the system displays the payment sheet, the Apple Pay JS // API calls your session object’s onvalidatemerchant event handler // to verify that the request is coming from a valid merchant. //=================================== session.onvalidatemerchant = async event => { //----------------------------------- // Call your own server to request a new merchant session //----------------------------------- const merchantSession = await validateMerchant(event.validationURL); //----------------------------------- // Pass opaque merchant object to ApplePaySDK to // complete merchant validation //----------------------------------- session.completeMerchantValidation(merchantSession); }; //=================================== // Event handler to call when the user selects a new payment method. //=================================== /* session.onpaymentmethodselected = event => { // Define ApplePayPaymentMethodUpdate based on the selected payment method. // No updates or errors are needed, pass an empty object. const update = {}; session.completePaymentMethodSelection(update); }; */ //=================================== // Event handler to call when user selects a shipping method //=================================== session.onshippingmethodselected = event => { //----------------------------------- // Define ApplePayShippingMethodUpdate based on the selected shipping method // No updates or errors are needed, pass an empty object. //----------------------------------- const update = {}; session.completeShippingMethodSelection(update); }; //=================================== // Event handler to call when user selects a shipping contact // in the payment sheet //=================================== session.onshippingcontactselected = event => { //----------------------------------- // Define ApplePayShippingContactUpdate based on the selected shipping // contact //----------------------------------- const update = {}; session.completeShippingContactSelection(update); }; //=================================== // An event handler the system calls when the user has authorized // the Apple Pay payment with Touch ID, Face ID, or a passcode. //=================================== session.onpaymentauthorized = event => { //----------------------------------- // Encrypted ApplePay Token //----------------------------------- const data = event.payment.token; //----------------------------------- // Endpoint for decrypting token //----------------------------------- fetch('yourAPIEndpoint', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) //----------------------------------- // Define what to do with decrypted token //----------------------------------- .then(res => console.log(res.json())) .catch(err => console.error(err)); //----------------------------------- //Define ApplePayPaymentAuthorizationResult //----------------------------------- const result = { "status": ApplePaySession.STATUS_SUCCESS }; session.completePayment(result); }; //=================================== // An event handler called by the system when the user // enters or updates a coupon code. //=================================== session.oncouponcodechanged = event => { //----------------------------------- // Define ApplePayCouponCodeUpdate //----------------------------------- const newTotal = calculateNewTotal(event.couponCode); const newLineItems = calculateNewLineItems(event.couponCode); const newShippingMethods = calculateNewShippingMethods(event.couponCode); const errors = calculateErrors(event.couponCode); session.completeCouponCodeChange({ newTotal: newTotal, newLineItems: newLineItems, newShippingMethods: newShippingMethods, errors: errors }); }; session.oncancel = event => { //----------------------------------- // Define behavior when payment cancelled by WebKit //----------------------------------- }; //=================================== // Start up SDK //=================================== session.begin(); };
Backend
Backend decrypts the Apple Pay token and sends it back to the frontend.
Backend - Required files
-
Apple Pay Merchant Identifier Certificate
- Use the same .pem file from Retrieve Encrypted Apple Pay Token
- You will need the merchant identifier of the public key to derive the symmetric key
-
Apple Pay Payment Processor Certificate
-
You will need the private key to generate the shared secret, which is needed to get your symmetric key
To download:
-
In Certificates, Identifiers & Profiles, click Identifiers in the sidebar, then select Merchant IDs from the pop-up menu on the top right.
-
On the right, select your merchant identifier.
-
Under Apple Pay Payment Processing Certificate, click Create Certificate.
-
Create a certificate signing request on your Mac, then click Continue.
-
Click Choose File.
-
In the dialog that appears, select the certificate request file (a file with a .certSigningRequest file extension), then click Choose.
-
Click Continue.
-
Click Download.
-
The certificate file (a file with a .cer file extension) appears in your Downloadsfolder.
Next, ensure you have your Payment Processing Certificate in pem format: -
Note: The file generated needs to be of format “.certSigningRequest”
-
On Apple Developer website, under Apple Pay Merchant Identity Certificate, click Create Certificate
-
Upload this “certSigningRequest” file
-
Download the resulting .cer file
-
Open the .cer file in Keychain Access application
-
Export both the crt and key files as a .pem file
-
-
AppleRootCA-G3 Certificate
- This certificate is required for signature verification
- Download here
Backend - Required dependencies
Reminder: install each of these dependencies by running npm install from your terminal
- crypto
- will be used to create our hash and decipher
- node-forge
- will be used to read our merchant identity certificate to extract info
- ec-key
- will be used to extract public and private keys
- asn1js
- will read our certificates in raw binary format and help convert to parseable data
- pkijs
- reads the raw binary from asn1js and converts to parseable data structure
- node-webcrypto-ossl
- Used in setEngine for our pkijs settings
Backend implementation
Verify Signature
-
We need to import all the required dependencies. Import them like so:
const cryptoNative = require('crypto'); const forge = require('node-forge'); const ECKey = require('ec-key'); const asn1js = require('asn1js'); const pkijs = require('pkijs'); const Crypto = require('node-webcrypto-ossl');
-
Create the following global variables. Their uses will be explained later in the tutorial:
const TOKEN_EXPIRE_WINDOW = 300000; // should be set to 5 minutes (300000 ms) per apple const LEAF_CERTIFICATE_OID = '1.2.840.113635.100.6.29'; const INTERMEDIATE_CA_OID = '1.2.840.113635.100.6.2.14'; const SIGNINGTIME_OID = '1.2.840.113549.1.9.5'; const MERCHANT_ID_FIELD_OID = '1.2.840.113635.100.6.32';
-
Create a new express endpoint ‘/decryptToken’. In the request, the backend will receive the encrypted Apple Pay token. The response will be the decrypted token. Your endpoint should start off like this:
app.post('/decryptToken', (req,res) => { });
-
We should instantiate a variable for the token. We can access the token via req.body.encryptedToken.
const token = req.body.encryptedToken;
-
Before proceeding with decryption, the documentation tells us to verify the signature. Create a new asynchronous function called verifySignature. It will take one argument, the encrypted token. We will conduct all the verification in this function.
const verifySignature = async (token) => { };
-
Call verifySignature within your endpoint and pass the token
verifySignature(token);
-
Ensure that the certificates contain the correct custom OIDs: 1.2.840.113635.100.6.29 for the leaf certificate and 1.2.840.113635.100.6.2.14 for the intermediate CA. The value for these marker OIDs doesn’t matter, only their presence.
We get the certificates from the token. -
First, create a Nodejs Buffer from our token signature. Buffers allow us to handle raw binary data. This should be converted from ‘base64’ format.
const cmsSignedBuffer = Buffer.from(token.signature, 'base64');
-
Our cmsSignedBuffer is now in Basic Encoding Rules (BER) format. We want to convert this to ASN.1 encoding. Transform it using the asn1js dependency like so
const cmsSignedASN1 = asn1js.fromBER(new Uint8Array(cmsSignedBuffer).buffer);
-
Use the cmsSignedASN1 result in pkijs content info:
const cmsContentSimpl = new pkijs.ContentInfo({ schema: cmsSignedASN1.result });
-
Use cmsContentSimpl content in pkijs signed data:
const cmsSignedData = new pkijs.SignedData({ schema: cmsContentSimpl.content });
Our variable cmsSignedData is now in a format that we can use!
-
cmsSignedData has a property certificates that we can use to check for correct OIDs. Create a function called checkCertificates. Call this function and pass cmsSignedData.certificates as the argument.
checkCertificates(cmsSignedData.certificates)
-
checkCertificates needs to verify if OID 1.2.840.113635.100.6.29 for the leaf certificate and 1.2.840.113635.100.6.2.14 for the intermediate certificate exist.
The length of the function argument certificates should be 2, the first being the leaf and the second being the intermediate. -
We can access the OID of each certificate via extensions.
-
We can use Javascript’s find function to check for these OIDs.
-
Thus, our resulting checkCertificates function should be defined as follows (we can use the global variables that we set previously to help readability):
const checkCertificates = (certificates) => { if (certificates.length !== 2) { throw new Error( `Signature certificates number error: expected 2 but got ${certificates.length}` ); } if ( !certificates[0].extensions.find(x => x.extnID === LEAF_CERTIFICATE_OID) ) { throw new Error( `Leaf certificate doesn't have extension: ${LEAF_CERTIFICATE_OID}` ); } if (!certificates[1].extensions.find(x => x.extnID === INTERMEDIATE_CA_OID)) { throw new Error( `Intermediate certificate doesn't have extension: ${INTERMEDIATE_CA_OID}` ); } }
-
Next, we need to ensure that the root CA is the Apple Root CA - G3. As a prerequisite, you needed to download the Root CA - G3 file from here. Once this is done, this condition is satisfied.
-
We need to ensure that there is a valid X.509 chain of trust from the signature to the root CA. We also need to validate the token’s signature
PKI.js can check chain of trust and verify at the same time. -
Create an asynchronous function called validateSignature. It will take 3 arguments: the cmsSignedData, the AppleRootCA, and signedData. We need to prepare AppleRootCA and the signedData to be read by PKI.js.
-
Create a global variable called AppleRootCABuffer. Set it equal to filesystem reading the AppleRootCA-G3.cer file:
const AppleRootCABuffer = fs.readFileSync('/path/to/cert/AppleRootCA-G3.cer');
-
AppleRootCABuffer will be in BER format. Convert it to ASN1:
const AppleRootCAASN1 = asn1js.fromBER(new Uint8Array(AppleRootCABuffer).buffer);
-
Read AppleRootCAASN1 into PKIJS as a certificate:
const AppleRootCA = new pkijs.Certificate({ schema: AppleRootCAASN1.result });
Your AppleRootCA is ready! We still need to prep the signedData.
-
The documentation states that for ECC (EC_v1), ensure that the signature is a valid Ellyptical Curve Digital Signature Algorithm (ECDSA) signature (ecdsa-with-SHA256 1.2.840.10045.4.3.2) of the concatenated values of the ephemeralPublicKey, data, transactionId, and applicationData keys.
We need to concatenate our ephemeralPublicKey, data, and transactionId in a Buffer.
-
First, create a Buffer from the token’s ephemeralPublicKey:
const p1 = Buffer.from(token.header.ephemeralPublicKey, 'base64');
-
Create a Buffer from the token’s data:
const p2 = Buffer.from(token.data, 'base64');
-
Create a Buffer from the token’s transactionID:
const p3 = Buffer.from(token.header.transactionId, 'hex');
-
Concatenate p1, p2, and p3 together:
const signedData = Buffer.concat([p1, p2, p3]);
This is our signature to use. We are ready to define validateSignature.
-
validateSignature uses cmsSignedData.verify() to do verification.
-
.verify() takes a dictionary.
- signer: set this as 0
- trustedCerts: this should be your rootCA
- data: this should be your signedData
- checkChain: set this to true. This checks the x509 chain of trust
- extendedMode: set this to true. This is to show the signature validation result.
-
Your function should thus be defined as:
// validateSignature - const validateSignature = (cmsSignedData, rootCA, signedData) => { return cmsSignedData.verify({ //=================================== // Should only contain 1 signer, verify with it //=================================== signer: 0, trustedCerts: [rootCA], data: signedData, //=================================== // Check x509 chain of trust //=================================== checkChain: true, //=================================== // Enable to show signature validation result //=================================== extendedMode: true, }); };
-
Within verifySignature, call it like so:
const ret = await validateSignature(cmsSignedData, AppleRootCA, signedData); if (!ret.signatureVerified) { throw new Error('CMS signed data verification failed'); }
-
Lastly, we need to inspect the Cryptographic Message Syntax (CMS) signing time of the signature. Define a function called checkSigningTime. It will take an argument called signerInfo.
-
signerInfo can be extracted via cmsSignedData:
const signerInfo = cmsSignedData.signerInfos[0];
-
Within checkSigningTime, we first need to access the signerInfo attributes
const signerInfoAttrs = signerInfo.signedAttrs.attributes;
-
Within those attributes, find the signing time OID (this should be a global variable that you set previously)
const attr = signerInfoAttrs.find(x => x.type === SIGNINGTIME_OID);
-
Convert the signed time into a Javascript Date object
const signedTime = new Date(attr.values[0].toDate());
-
Set another variable for now
const now = new Date();
-
Now, you need to see if the difference between now and signedTime is greater than the token expiration window. Apple says it should not differ by more than 5 minutes (or 300000 milliseconds). Throw an error if true.
if (now - signedTime > TOKEN_EXPIRE_WINDOW) { throw new Error('Signature has expired'); }
Note: TOKEN_EXPIRE_WINDOW was a global variable set as 300000
-
Call your checkSigningTime function within your verifySignature function. Pass signerInfo as the argument.
checkSigningTime(signerInfo);
-
You’re done with signature verification! Your final verifySignature function should be:
// verifySignature - const verifySignature = async (token) => { //=================================== // Extract data from token //=================================== const p1 = Buffer.from(token.paymentData.header.ephemeralPublicKey, 'base64'); const p2 = Buffer.from(token.paymentData.data, 'base64'); const p3 = Buffer.from(token.paymentData.header.transactionId, 'hex'); const signedData = Buffer.concat([p1, p2, p3]); //----------------------------------- // Create CMS Signed Data //----------------------------------- const cmsSignedBuffer = Buffer.from(token.paymentData.signature, 'base64'); const cmsSignedASN1 = asn1js.fromBER(new Uint8Array(cmsSignedBuffer).buffer); const cmsContentSimpl = new pkijs.ContentInfo({ schema: cmsSignedASN1.result, }); const cmsSignedData = new pkijs.SignedData({ schema: cmsContentSimpl.content, }); const signerInfo = cmsSignedData.signerInfos[0]; //----------------------------------- // 1.a Ensure that the certificates contain the correct custom OIDs: 1.2.840.113635.100.6.29 // for the leaf certificate and 1.2.840.113635.100.6.2.14 for the intermediate CA //----------------------------------- checkCertificates(cmsSignedData.certificates); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 1.b Ensure that the root CA is the Apple Root CA * G3 - root CA downloaded from Apple web site so this is satisfied // 1.c Ensure that there is a valid X.509 chain of trust from the signature to the root CA // 1.d Validate the token’s signature // PKI.js can check chain of trust and verify on one shot, so 1.c and 1.d can be done together //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ const ret = await validateSignature(cmsSignedData, AppleRootCA, signedData); if (!ret.signatureVerified) { throw new Error('CMS signed data verification failed'); } //::::::::::::::::::::::::::::::::::: // 1.e Inspect the CMS signing time of the signature //::::::::::::::::::::::::::::::::::: checkSigningTime(signerInfo); };