Series intro: Apple Pay Certificate Signing Using .NET
At this point, we’ve uploaded our “payment processing” and/or “merchant identity” CSR to Apple (or had our merchant do that on our behalf), Apple has signed our CSR using their CA (Certificate Authority) private key, and we have downloaded the final Apple-issued certificate (.cer extension) to our system. Apple’s guide pretty much ends at that point, but we have some more work to do to really make this useful, primarily combining that certificate Apple gave us with the private key we generated.
This task should be simple, but there’s a limitation in .NET which is going to make it a bit tricky for us. We have three extension methods for combining our private key with our certificate: RSACertificateExtensions.CopyWithPrivateKey, ECDsaCertificateExtensions.CopyWithPrivateKey, & DSACertificateExtensions.CopyWithPrivateKey.
RSACertificateExtensions.CopyWithPrivateKey is exactly what we need for our “merchant identity” certificates. Just pass in our RSA private key and save off the resulting certificate:
using X509Certificate2 CertificateWithPrivateKey = ImportedSignedCertificate.CopyWithPrivateKey(RSAPrivateKey); byte[] CertificateData = CertificateWithPrivateKey.Export(X509ContentType.Pfx, CertificatePassword);
Cakewalk!
Our “payment processing” certificate is more difficult. In this case, Apple has given us an ECC certificate with very specific key usage flags set. Turns out .NET doesn’t give us a way to access that! ECDsaCertificateExtensions.CopyWithPrivateKey really could work, but the code is checking for key usage flags specific to signing, where our certificate is used for deriving key material. I have opened an issue on the dotnet/runtime repo to try and get the API expanded to support what we need to work with these certificates.
If you look at the source to those extensions, they are just calling into the native libraries so the good news is we can do this too in order to get around the API block. Basically, we have to open a handle to our certificate using CertDuplicateCertificateContext and then set the private key to an NCryptKey using CertSetCertificateContextProperty.
Let’s see how it looks:
using NativeMethods.SafeCertContextHandle CertificateContext = NativeMethods.GetCertificateContext(ImportedSignedCertificate); using SafeNCryptKeyHandle KeyHandle = ECDsaPrivateKey.Key.Handle; if (!NativeMethods.CertSetCertificateContextProperty( CertificateContext, NativeMethods.CertificateProperty.NCryptKeyHandle, NativeMethods.CertificateSetPropertyFlags.CERT_SET_PROPERTY_INHIBIT_PERSIST_FLAG, KeyHandle)) { throw new InvalidOperationException($"ECDsa private key could not be set on Certificate Win32Error [{Marshal.GetLastWin32Error()}] returned."); } using X509Certificate2 CertificateWithPrivateKey = new X509Certificate2(CertificateContext.DangerousGetHandle()); byte[] CertificateData = CertificateWithPrivateKey.Export(X509ContentType.Pfx, CertificatePassword);
More work, but not too bad.
Where do we go from here? Well, you are going to need these certs to do your Apple Pay processing, so you have to put them somewhere. We have a database-backed certificate repository. You can use the stores provided by Windows. You can load them off the file system, if you really want to. Talk to your security team and come up with a strategy! Keep in mind the certificates issued by Apple don’t last forever, so you will also need a rotation strategy.
Here are the native definitions:
using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography.X509Certificates; using Microsoft.Win32.SafeHandles; namespace Macross { internal static partial class NativeMethods { public enum CertificateProperty { KeyProviderInfo = 2, // CERT_KEY_PROV_INFO_PROP_ID KeyContext = 5, // CERT_KEY_CONTEXT_PROP_ID NCryptKeyHandle = 78, // CERT_NCRYPT_KEY_HANDLE_PROP_ID } [Flags] public enum CertificateSetPropertyFlags { CERT_SET_PROPERTY_INHIBIT_PERSIST_FLAG = 0x40000000, None = 0x00000000, } [DllImport("crypt32.dll")] public static extern SafeCertContextHandle CertDuplicateCertificateContext(IntPtr certContext); [DllImport("crypt32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CertGetCertificateContextProperty( SafeCertContextHandle pCertContext, CertificateProperty dwPropId, [Out] out IntPtr pvData, [In, Out] ref int pcbData); [DllImport("crypt32.dll", SetLastError = true), ResourceExposure(ResourceScope.None)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CertFreeCertificateContext(IntPtr pCertContext); [DllImport("crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CertSetCertificateContextProperty( SafeCertContextHandle pCertContext, CertificateProperty dwPropId, CertificateSetPropertyFlags dwFlags, [In] SafeNCryptKeyHandle keyHandle); internal static SafeCertContextHandle GetCertificateContext(X509Certificate certificate) { SafeCertContextHandle certificateContext = CertDuplicateCertificateContext(certificate.Handle); // Make sure to keep the X509Certificate object alive until after its certificate context is // duplicated, otherwise it could end up being closed out from underneath us before we get a // chance to duplicate the handle. GC.KeepAlive(certificate); return certificateContext; } internal sealed class SafeCertContextHandle : SafeHandleZeroOrMinusOneIsInvalid { private SafeCertContextHandle() : base(true) { } protected override bool ReleaseHandle() => CertFreeCertificateContext(handle); } } }