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.” The “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. For details on that process, see: Apple Pay Certificate Signing Using .NET. 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!

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

  • Question : Do we need to upload Payment processing certificate at our payment service provider ?

    As you have written in above blog, our product has a DB repository so for us our Apple “payment processing” certificates are uploaded into that. Need some explanation about this. The code written above clearly searches repository/DB storage for finding correct payment processing certificate.

    We have created ‘Payment Processing Certificate’. But we have not given it to the Payment service provider ( who decrypts token data supplied by us). In our case Payment service provider is WorldPay. Is it necessary to store it at worldpay site.

    It is neither mentioned clearly on their (WorldPay) site nor it is specified directly on Apple Developer site.

    • Hey Vishnudas!

      I talk a little bit about the certificate types here: https://blog.macrosssoftware.com/index.php/2019/12/04/apple-pay-certificate-signing-using-net-part-1-generate-a-private-key-and-a-certificate-signing-request-to-upload-to-apple/

      The short answer is, yes, give your payment processing certificate to WorldPay. They will need it to decrypt and process your transactions.

      My ApplePay posts are written from the perspective of the payment gateway, which is WorldPay in your case.

      ApplyPay is kind of funny in that the merchant has to do the certificate management. You get an identity certificate and a payment processing certificate through the Apple portal. The identity certificate the merchant uses to get blobs. The payment processing certificate is what the processor (usually not the merchant) needs, but they have to get it from the merchant, not Apple.

      We were talking to Apple about opening up the process to intermediaries/service providers but I’m no longer involved so I don’t know the state of the project.

      I hope that was helpful!

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.