Decrypting Apple Pay Payment Blob Using .NET – Part 2: Finding the merchant public key.

Series Intro: Decrypting Apple Pay Payment Blob Using .NET

Step 2 on Apple’s guide for decrypting the payment blob is probably the easiest. Basically we need to retrieve the Payment processing certificate that we will use to perform the decryption.

Apple’s Configuring Your Environment page covers two types of certificates: Payment processing & Merchant identity. Merchant identity certificate is used on the front-end when creating the Apple Pay session, which is how the user interacts with Apple via their device. The Payment processing certificate is used to encrypt/decrypt the final data at the end of the process. The merchant typically passes their blobs to a payment processor to do decryption and process payment on their behalf. That’s where we come in!

We own the Payment processing certificate. Apple never sees the private key, part of the setup process is to send them a CSR so that we can combine the Apple signed certificate with our protected private secret. It is up to us to store this key somewhere (securely) where we can retrieve it as needed to do our decryption. Certificate management is out of the scope of this series, talk to your security team about it! Our product has a DB repository so for us our Apple Payment processing certificates are uploaded into that. Windows provides machine stores which you can easily use from .NET. You can also load certificates as password-protected files.

One of the header keys on the JSON is publicKeyHash. Defined as:

KeyValueDescription
publicKeyHashSHA–256 hash, Base64 encoded as a stringHash of the X.509 encoded public key bytes of the merchant’s certificate.

What that all means is our task is to look for a Payment processing certificate in our repository with a hash that matches. Here’s some code to show (more or less) how it could look:

        public static X509Certificate2 FindAndValidatePaymentProcessingCertificate(string publicKeyHash)
        {
            byte[] SuppliedCertificatePublicKeyHash = Convert.FromBase64String(publicKeyHash);

            // FindApplePayPaymentProcessingCertificates returns Apple X509 payment processing certificates from some kind of repository. DB, file system, embedded resources, be creative my friends!
            X509Certificate2Collection PaymentProcessingCertificates = FindApplePayPaymentProcessingCertificates();

            X509Certificate2 MatchedDecryptionCertificate = null;
            foreach (X509Certificate2 Certificate in PaymentProcessingCertificates)
            {
                using (HashAlgorithm SHA = new SHA256CryptoServiceProvider())
                {
                    byte[] CalculatedHash = SHA.ComputeHash(Certificate.ExportPublicKeyInDERFormat());

                    if (SuppliedCertificatePublicKeyHash.SequenceEqual(CalculatedHash))
                    {
                        MatchedDecryptionCertificate = Certificate;
                        break;
                    }
                }
            }

            if (MatchedDecryptionCertificate == null)
                throw new InvalidOperationException("No payment processing certificate could be found matching the publicKeyHash on the payment data.");

            if (!MatchedDecryptionCertificate.HasPrivateKey)
                throw new InvalidOperationException("Payment processing certificate was found matching the publicKeyHash on the payment data but it does not have a private key.");

            return MatchedDecryptionCertificate;
        }

Note: The code above uses SHA256CryptoServiceProvider because our servers have “FIPS mode” enabled in order to support some government customers. If you don’t have to, use SHA256Managed instead. AFAIK it is faster.

We have now run into our first .NET roadblock! Apple’s hash isn’t of the public key bytes, it is of the certificate in DER format. .NET doesn’t have anything built-in for exporting certificates in DER (or PEM) format. You can use another library, like BouncyCastle or OpenSSL, to augment what .NET provides, or roll your own. The format is pretty simple, here’s a set of extension methods you can add to your projects to get up and running:

using System;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Macross
{
	public static class CertificateExtensions
	{
		public static byte[] ExportPublicKeyInDERFormat(this X509Certificate certificate)
		{
			if (certificate == null)
				throw new ArgumentNullException(nameof(certificate));

			byte[] algOid = CryptoConfig.EncodeOID(certificate.GetKeyAlgorithm());

			byte[] algParams = certificate.GetKeyAlgorithmParameters();

			byte[] algId = BuildSimpleDERSequence(algOid, algParams);

			byte[] publicKey = WrapAsBitString(certificate.GetPublicKey());

			return BuildSimpleDERSequence(algId, publicKey);
		}

		public static string ExportPublicKeyInPEMFormat(this X509Certificate certificate)
			=> PEMEncode(ExportPublicKeyInDERFormat(certificate), "PUBLIC KEY");

		private static string PEMEncode(byte[] derData, string pemLabel)
		{
			StringBuilder builder = new StringBuilder();
			builder.Append("-----BEGIN ");
			builder.Append(pemLabel);
			builder.AppendLine("-----");
			builder.AppendLine(Convert.ToBase64String(derData, Base64FormattingOptions.InsertLineBreaks));
			builder.Append("-----END ");
			builder.Append(pemLabel);
			builder.AppendLine("-----");

			return builder.ToString();
		}

		private static byte[] BuildSimpleDERSequence(params byte[][] values)
		{
			int totalLength = values.Sum(v => v.Length);
			byte[] len = EncodeDERLength(totalLength);
			int offset = 1;

			byte[] seq = new byte[totalLength + len.Length + 1];
			seq[0] = 0x30;

			Buffer.BlockCopy(len, 0, seq, offset, len.Length);
			offset += len.Length;

			foreach (byte[] value in values)
			{
				Buffer.BlockCopy(value, 0, seq, offset, value.Length);
				offset += value.Length;
			}

			return seq;
		}

		private static byte[] WrapAsBitString(byte[] value)
		{
			byte[] len = EncodeDERLength(value.Length + 1);
			byte[] bitString = new byte[value.Length + len.Length + 2];
			bitString[0] = 0x03;
			Buffer.BlockCopy(len, 0, bitString, 1, len.Length);
			bitString[len.Length + 1] = 0x00;
			Buffer.BlockCopy(value, 0, bitString, len.Length + 2, value.Length);
			return bitString;
		}

		private static byte[] EncodeDERLength(int length)
		{
			if (length <= 0x7F)
			{
				return new byte[] { (byte)length };
			}

			if (length <= 0xFF)
			{
				return new byte[] { 0x81, (byte)length };
			}

			if (length <= 0xFFFF)
			{
				return new byte[]
				{
					0x82,
					(byte)(length >> 8),
					(byte)length,
				};
			}

			if (length <= 0xFFFFFF)
			{
				return new byte[]
				{
					0x83,
					(byte)(length >> 16),
					(byte)(length >> 8),
					(byte)length,
				};
			}

			return new byte[]
			{
				0x84,
				(byte)(length >> 24),
				(byte)(length >> 16),
				(byte)(length >> 8),
				(byte)length,
			};
		}
	}
}

I know that was modded/adapted from other posts around the internet but I don’t have citations available because it was a while ago. Thank you original authors for the help, sorry for the lack of credit!

That’s basically it for step 2. Now on to the real fun, decryption!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.