Skip to main content

Payment method encryption

The Shop Pay Wallet will return plain (untokenized) credit card information for use in processing payments. This will consist of:

This setup allows your integration to be used for placing orders with both Shopify and non-Shopify merchants on an external surface, since the information returned can be used within any payment processor to extract payment. But, for the same reason, it also presents a significant security risk.

Your requests to our API need to occur via an encrypted SSL/TLS HTTP connection, which ensures a layer of security. But, in order to further mitigate the risks of passing raw PANs over the wire, we have added an additional layer of encryption to protect this data. We use an integrated encryption process based on the Elliptic Curve Integrated Encryption Scheme (ECIES) to additionally encrypt the information required to process payment.

With this setup, we guarantee the following:

  • All requests between your systems and the Shop Pay Wallet are done using a secure connection
  • We'll only return encrypted payment information if we can verify your system's identity
  • We'll never return unencrypted payment information to you
  • Only your system that requested payment information from the Shop Pay Wallet will be able to decrypt the payload we provide

This guide explains how to work with this payment method encryption setup. This guide uses OpenSSL and Ruby in its examples.


This section details setup steps you must take to request and decrypt encrypted payment information from the Shop Pay Wallet. You will need to work with a Shop team member for part of this.

The end result of this setup work will be:

  • A private encryption key known only to you, which your systems will use to decrypt and verify the payload
  • A PEM-formatted public certificate signed by Shopify's Certificate Authority (CA), which Shop Pay will use to encrypt the payload

Anchor to Step 1: Generate a public/private key-pair and CSRStep 1: Generate a public/private key-pair and CSR

You'll start by generating a public/private elliptic curve (EC) keypair using the NIST P-256 method, which is implemented as the prime256v1 extension in OpenSSL. You'll use both keys to additionally generate a Certificate Signing Request (CSR), which will later be exchanged with Shopify for a PEM-formatted public certificate used in encryption requests. You'll also later use the private key during the decryption process.

It's important that the CSR has a complete Subject and that the Subject's CN is formatted as Shopify Payments Partner - {partner_name}.

You can generate a CSR that has a shopping platform named Foobar Shopping, which is maintained by Company, Inc., and headquartered in CA (Canada), ON (Ontario), Ottawa, using the following Ruby code or the openssl shell command:

Generate a CSR

keypair.rb

require 'openssl'

# Generate the keypair
key = OpenSSL::PKey::EC.generate('prime256v1')
ec_group = OpenSSL::PKey::EC::Group.new('prime256v1')

# Build the public key for signing the CSR
public_key = OpenSSL::PKey::EC.new(ec_group)
public_key.public_key = OpenSSL::PKey::EC::Point.new(
ec_group,
OpenSSL::BN.new(key.public_key.to_bn.to_s(16), 16)
)

# Build the private key for signing the CSR
private_key = OpenSSL::PKey::EC.new(ec_group)
private_key.private_key = OpenSSL::BN.new(key.private_key.to_s(16), 16)

# Define the Subject
csr_subject = '/C=CA/ST=ON/L=Ottawa/O=Company, Inc./OU=Shopping/CN=Shopify Payments Partner - Foobar Shopping'

# Create and sign the CSR
csr = OpenSSL::X509::Request.new
csr.version = 1
csr.subject = OpenSSL::X509::Name.parse(csr_subject)
csr.public_key = public_key
csr.sign(private_key, OpenSSL::Digest::SHA256.new)

# Write the private key and CSR to files for later use
File.open('./private_key.pem', 'wb') { |f| f.print(key.to_pem) }
File.open('./csr.pem', 'wb') { |f| f.print(csr.to_pem) }
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem
openssl ec -in private_key.pem -pubout -out public_key.pem

openssl req \
-new \
-sha256 \
-key private_key.pem \
-out csr.pem \
-subj "/C=CA/ST=ON/L=Ottawa/O=Company, Inc./OU=Shopping/CN=Shopify Payments Partner - Foobar Shopping"

The methods above, for example, create an EC private key file and a CSR file with the following contents:

Examples of an EC private key file and a CSR file

EC private key file

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIAJa4IU/2qt/Eot8+/tqvyDZwF6Bm2gn17L0tq3LWNFroAoGCCqGSM49
AwEHoUQDQgAE64Qk3dBVKeP9WajKkhNNtLltP8UWZfbG50nTRyUuJ7X06yrTlFzR
b+HtarJjDWcD9kTNi+mz+1kiQE57L3HuLw==
-----END EC PRIVATE KEY-----

CSR file

-----BEGIN CERTIFICATE REQUEST-----
MIIBRjCB7gIBATCBizELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMQ8wDQYDVQQH
DAZPdHRhd2ExFjAUBgNVBAoMDUNvbXBhbnksIEluYy4xETAPBgNVBAsMCFNob3Bw
aW5nMTMwMQYDVQQDDCpTaG9waWZ5IFBheW1lbnRzIFBhcnRuZXIgLSBGb29iYXIg
U2hvcHBpbmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATrhCTd0FUp4/1ZqMqS
E020uW0/xRZl9sbnSdNHJS4ntfTrKtOUXNFv4e1qsmMNZwP2RM2L6bP7WSJATnsv
ce4voAAwCgYIKoZIzj0EAwIDRwAwRAIgdZuoUvDo5G9810z9UDRgKVhy6QqY5UMW
0owwbapNr5cCIGNB3E8zQWawwQpjsHgFSt1QN5Nl7ZCmUtY1C9gx3zBo
-----END CERTIFICATE REQUEST-----

You can introspect these two credentials to assert that the CSR signing process was successful. When you introspect the private key, you should see a pub and priv value:

Introspecting the EC private key and the CSR

introspect_key.rb

require 'openssl'

private_key = OpenSSL::PKey::EC.new(File.read('./private_key.pem'))
puts private_key.to_text
openssl pkey -noout -text -in private_key.pem

EC private Key

Private-Key: (256 bit)
priv:
02:5a:e0:85:3f:da:ab:7f:12:8b:7c:fb:fb:6a:bf:
20:d9:c0:5e:81:9b:68:27:d7:b2:f4:b6:ad:cb:58:
d1:6b
pub:
04:eb:84:24:dd:d0:55:29:e3:fd:59:a8:ca:92:13:
4d:b4:b9:6d:3f:c5:16:65:f6:c6:e7:49:d3:47:25:
2e:27:b5:f4:eb:2a:d3:94:5c:d1:6f:e1:ed:6a:b2:
63:0d:67:03:f6:44:cd:8b:e9:b3:fb:59:22:40:4e:
7b:2f:71:ee:2f
ASN1 OID: prime256v1
NIST CURVE: P-256

Then, if you introspect the CSR, you should see:

  1. The same pub value encoded into the CSR
  2. The full Subject string

File

introspect_csr.rb

require 'openssl'

csr = OpenSSL::X509::Request.new(File.read('./csr.pem'))
puts csr.to_text
openssl req -noout -text -in csr.pem

CSR

Certificate Request:
Data:
Version: 1 (0x1)
Subject: C=CA, ST=ON, L=Ottawa, O=Company, Inc., OU=Shopping, CN=Shopify Payments Partner - Foobar Shopping
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:eb:84:24:dd:d0:55:29:e3:fd:59:a8:ca:92:13:
4d:b4:b9:6d:3f:c5:16:65:f6:c6:e7:49:d3:47:25:
2e:27:b5:f4:eb:2a:d3:94:5c:d1:6f:e1:ed:6a:b2:
63:0d:67:03:f6:44:cd:8b:e9:b3:fb:59:22:40:4e:
7b:2f:71:ee:2f
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
a0:00
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:75:9b:a8:52:f0:e8:e4:6f:7c:d7:4c:fd:50:34:
60:29:58:72:e9:0a:98:e5:43:16:d2:8c:30:6d:aa:4d:af:97:
02:20:63:41:dc:4f:33:41:66:b0:c1:0a:63:b0:78:05:4a:dd:
50:37:93:65:ed:90:a6:52:d6:35:0b:d8:31:df:30:68

Before moving on to the next step, you must preserve the private key in a secure location. You will later use the private key to decrypt and verify encrypted payment payloads.

Anchor to Private key formattingPrivate key formatting

OpenSSL and elliptic curve encryption support multiple formats for private keys. In the examples above, we generate and use a private key using Elliptic Curve (EC) format. You can use EC private keys only for elliptic curve encryption. Private keys in the EC format have the following PEM-file header:

EC Private Key

-----BEGIN EC PRIVATE KEY-----

Another commonly used private key format is PKCS8 formatting. PKCS8 is a generic formatting, meaning private keys in this format work with multiple encryption schemes. These private keys are lengthier when encoded into a PEM-file, and have a different PEM-file header:

PKCS8 Private Key

-----BEGIN PRIVATE KEY-----

You can use either of these two formats for the private key you generate for the Shop Pay Wallet ECIES. We recognize that some teams have strong opinions on private key formats. If you wish to use PKCS8 formatting specifically, you can convert an EC formatted private key PEM-file into a PKCS8 formatted PEM-file using the following OpenSSL command:

to_pkcs8.sh

openssl pkcs8 -topk8 -nocrypt -in private_key.pem -out private_key_pkcs8.pem

If we were to convert our example EC formatted private key from above into a PKCS8 formatted key, it should look like the following:

PKCS8 Private Key

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgAlrghT/aq38Si3z7
+2q/INnAXoGbaCfXsvS2rctY0WuhRANCAATrhCTd0FUp4/1ZqMqSE020uW0/xRZl
9sbnSdNHJS4ntfTrKtOUXNFv4e1qsmMNZwP2RM2L6bP7WSJATnsvce4v
-----END PRIVATE KEY-----

You need to convert your EC private key to a PKCS8 private key before signing your CSR with your private key.

Anchor to Private key signing bytesPrivate key signing bytes

In the examples above, we are using a private key without a signing byte.

Specific versions of OpenSSL preprend a "signing byte" to private key values. The signing byte indicates the private key version. The signing byte is not actually part of the private key, but rather encoded into the PEM-file and included in the introspected textual output of the private key in OpenSSL commands.

As a comparative example, here is an EC formatted private key encoded with a "signing byte" in a PEM-file:

EC Private Key (with signing byte)

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJ3d4MDjMYtfq4an8J7biudUvr4qD5Hwjp+5WNaMovbyoAoGCCqGSM49
AwEHoUQDQgAEr96eU7+1mx7gTV0V8T3wdtN5/fOxGZP+jGmk4Db4N7LMUUF43fYn
eTINwcHdaLOrpFqcTswPSqDFd61V6PEqFw==
-----END EC PRIVATE KEY-----

-----BEGIN CERTIFICATE----- MIICDDCCAbOgAwIBAgIUR352YtBzbvY9dhcRHwn4lWyinaQwCgYIKoZIzj0EAwIw bzELMAkGA1UEBhMCQ0ExEDAOBgNVBAgTB09udGFyaW8xDzANBgNVBAcTBk90dGF3 YTEQMA4GA1UEChMHU2hvcGlmeTErMCkGA1UEAxMiU2hvcGlmeSBQYXltZW50IFBh cnRuZXIgQ0EgU3RhZ2luZzAeFw0yMTA5MjMxMjEzMzdaFw0yMjA5MjMxMjE0MDda MDUxMzAxBgNVBAMTKlNob3BpZnkgUGF5bWVudHMgUGFydG5lciAtIEZvb2JhciBT aG9wcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOuEJN3QVSnj/VmoypIT TbS5bT/FFmX2xudJ00clLie19Osq05Rc0W/h7WqyYw1nA/ZEzYvps/tZIkBOey9x 7i+jZzBlMA4GA1UdDwEB/wQEAwIDuDATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNV HQ4EFgQUeI1U+LdZT6+qBN8VRK6tdW4/hvowHwYDVR0jBBgwFoAUkIAYCIcnGnIa a0z8DtxEg//XLWYwCgYIKoZIzj0EAwIDRwAwRAIga+EqwdSNtbdVZ3r+7yPoMFVk ubv/HO2+zH/BMzt247sCICWHJCXv74zrZccSWFuPKBBtywTEa67S9GKlfL4+q+kQ -----END CERTIFICATE-----

And here is that same private key introspected into a textual output. Notice the 00: prefixing the private key text.

EC Private Key (with signing byte)

Private-Key: (256 bit)
priv:
00:9d:dd:e0:c0:e3:31:8b:5f:ab:86:a7:f0:9e:db:
8a:e7:54:be:be:2a:0f:91:f0:8e:9f:b9:58:d6:8c:
a2:f6:f2
pub:
04:af:de:9e:53:bf:b5:9b:1e:e0:4d:5d:15:f1:3d:
f0:76:d3:79:fd:f3:b1:19:93:fe:8c:69:a4:e0:36:
f8:37:b2:cc:51:41:78:dd:f6:27:79:32:0d:c1:c1:
dd:68:b3:ab:a4:5a:9c:4e:cc:0f:4a:a0:c5:77:ad:
55:e8:f1:2a:17
ASN1 OID: prime256v1
NIST CURVE: P-256

Similar to how we support both EC and PKCS8 private key formats, you can use private keys with or without a signing byte encoded.

Anchor to Step 2: Joint validation of the CSRStep 2: Joint validation of the CSR

Next, you should share the CSR PEM-file with Shopify. This should be done via a secure channel, such as company email.

Before you generate a PEM-formatted public certificate signed by Shopify's CA, both parties must jointly verify the CSR's authenticity using a separate communications platform. You should verify the CSR's authenticity on a separate communication platform to ensure that the CSR wasn't tampered with when the CSR was shared with Shopify.

You should generate a fingerprint of the CSR file, so you can verify that you and a Shopify team member have generated identical CSR fingerprints. For example, you can use Ruby or the openssl shell command to generate a fingerprint of the CSR:

Generate a fingerprint of a CSR file

csr_to_text.rb

csr_to_text.rb

require 'openssl'

puts OpenSSL::Digest::SHA256.new(File.read('./csr.pem'))
openssl dgst -sha256 csr.pem

Result

SHA256(csr.pem)= f1e1279b4fe392bcc0123cc96c01a3430d9600cc22c7fc9ca6a34be6208c0a11

Anchor to Step 3: Shopify generates a public certificateStep 3: Shopify generates a public certificate

Next, a Shopify team member will use your CSR to generate a public certificate signed by Shopify's Certificate Authority. They will share it back with you in a PEM-file.

The following text is an example of a PEM-formatted public certificate:

Public Certificate

-----BEGIN CERTIFICATE-----
MIICDDCCAbOgAwIBAgIUR352YtBzbvY9dhcRHwn4lWyinaQwCgYIKoZIzj0EAwIw
bzELMAkGA1UEBhMCQ0ExEDAOBgNVBAgTB09udGFyaW8xDzANBgNVBAcTBk90dGF3
YTEQMA4GA1UEChMHU2hvcGlmeTErMCkGA1UEAxMiU2hvcGlmeSBQYXltZW50IFBh
cnRuZXIgQ0EgU3RhZ2luZzAeFw0yMTA5MjMxMjEzMzdaFw0yMjA5MjMxMjE0MDda
MDUxMzAxBgNVBAMTKlNob3BpZnkgUGF5bWVudHMgUGFydG5lciAtIEZvb2JhciBT
aG9wcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOuEJN3QVSnj/VmoypIT
TbS5bT/FFmX2xudJ00clLie19Osq05Rc0W/h7WqyYw1nA/ZEzYvps/tZIkBOey9x
7i+jZzBlMA4GA1UdDwEB/wQEAwIDuDATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNV
HQ4EFgQUeI1U+LdZT6+qBN8VRK6tdW4/hvowHwYDVR0jBBgwFoAUkIAYCIcnGnIa
a0z8DtxEg//XLWYwCgYIKoZIzj0EAwIDRwAwRAIga+EqwdSNtbdVZ3r+7yPoMFVk
ubv/HO2+zH/BMzt247sCICWHJCXv74zrZccSWFuPKBBtywTEa67S9GKlfL4+q+kQ
-----END CERTIFICATE-----

You can also introspect the public certificate to validate that it's encoded with the expected metadata. In the example above, the introspected public certificate has the following contents:

  • The private key's pub fingerprint under the Subject Public Key Info section.
  • The CSR's Subject CN value (Shopify Payments Partner - Foobar Shopping).
  • An Issuer that identifies Shopify's CA. In the examples below, the CA is from Shopify's staging environment. In production, the CA signature looks different.

File

introspect_cert.rb

require 'openssl'

cert = OpenSSL::X509::Certificate.new(File.read('./cert.pem'))
puts cert.to_text
openssl x509 -noout -text -in cert.pem

Public Certificate

Certificate:
Data:
Version: 3 (0x2)
Serial Number:
47:7e:76:62:d0:73:6e:f6:3d:76:17:11:1f:09:f8:95:6c:a2:9d:a4
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=CA, ST=Ontario, L=Ottawa, O=Shopify, CN=Shopify Payment Partner CA Staging
Validity
Not Before: Sep 23 12:13:37 2021 GMT
Not After : Sep 23 12:14:07 2022 GMT
Subject: CN=Shopify Payments Partner - Foobar Shopping
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:eb:84:24:dd:d0:55:29:e3:fd:59:a8:ca:92:13:
4d:b4:b9:6d:3f:c5:16:65:f6:c6:e7:49:d3:47:25:
2e:27:b5:f4:eb:2a:d3:94:5c:d1:6f:e1:ed:6a:b2:
63:0d:67:03:f6:44:cd:8b:e9:b3:fb:59:22:40:4e:
7b:2f:71:ee:2f
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Data Encipherment, Key Agreement
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Subject Key Identifier:
78:8D:54:F8:B7:59:4F:AF:AA:04:DF:15:44:AE:AD:75:6E:3F:86:FA
X509v3 Authority Key Identifier:
keyid:90:80:18:08:87:27:1A:72:1A:6B:4C:FC:0E:DC:44:83:FF:D7:2D:66

Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:6b:e1:2a:c1:d4:8d:b5:b7:55:67:7a:fe:ef:23:
e8:30:55:64:b9:bb:ff:1c:ed:be:cc:7f:c1:33:3b:76:e3:bb:
02:20:25:87:24:25:ef:ef:8c:eb:65:c7:12:58:5b:8f:28:10:
6d:cb:04:c4:6b:ae:d2:f4:62:a5:7c:be:3e:ab:e9:10

Anchor to Generating the Encryption Certificate HTTP headerGenerating the Encryption Certificate HTTP header

Before you confirm an order with Shop Pay to receive the encrypted credit card information, you'll need to ensure that the HTTP request's Certificate header for the Confirm an order and retrieve payment endpoint is using the correct format.

When you make an HTTP request to this endpoint, you need to include the public certificate using one of the following methods:

  • Encode the PEM-formatted public certificate as a Base64-encoded string in an HTTP request header named Certificate

  • Send the PEM-formatted public certificate body that has the newlines stripped from it in an HTTP request header named Certificate

    The following text is an example of a PEM-formatted public certificate:

certificate.pem

-----BEGIN CERTIFICATE-----
MIICDDCCAbOgAwIBAgIUR352YtBzbvY9dhcRHwn4lWyinaQwCgYIKoZIzj0EAwIw
bzELMAkGA1UEBhMCQ0ExEDAOBgNVBAgTB09udGFyaW8xDzANBgNVBAcTBk90dGF3
YTEQMA4GA1UEChMHU2hvcGlmeTErMCkGA1UEAxMiU2hvcGlmeSBQYXltZW50IFBh
cnRuZXIgQ0EgU3RhZ2luZzAeFw0yMTA5MjMxMjEzMzdaFw0yMjA5MjMxMjE0MDda
MDUxMzAxBgNVBAMTKlNob3BpZnkgUGF5bWVudHMgUGFydG5lciAtIEZvb2JhciBT
aG9wcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOuEJN3QVSnj/VmoypIT
TbS5bT/FFmX2xudJ00clLie19Osq05Rc0W/h7WqyYw1nA/ZEzYvps/tZIkBOey9x
7i+jZzBlMA4GA1UdDwEB/wQEAwIDuDATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNV
HQ4EFgQUeI1U+LdZT6+qBN8VRK6tdW4/hvowHwYDVR0jBBgwFoAUkIAYCIcnGnIa
a0z8DtxEg//XLWYwCgYIKoZIzj0EAwIDRwAwRAIga+EqwdSNtbdVZ3r+7yPoMFVk
ubv/HO2+zH/BMzt247sCICWHJCXv74zrZccSWFuPKBBtywTEa67S9GKlfL4+q+kQ
-----END CERTIFICATE-----

You can encode the PEM-formatted public certificate as Base64, and then use the Base64-encoded, PEM-formatted public certificate as your HTTP request's Certificate header.

To encode the PEM-formatted public certificate as Base64, you can use the base64 shell command:

Example of how to encode a PEM-formatted public certificate as Base64

Shell command

base64 certificate.pem

Example of a Base64-encoded public certificate

LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNERENDQWJPZ0F3SUJBZ0lVUjM1Mll0Qnpidlk5ZGhjUkh3bjRsV3lpbmFRd0NnWUlLb1pJemowRUF3SXcKYnpFTE1Ba0dBMVVFQmhNQ1EwRXhFREFPQmdOVkJBZ1RCMDl1ZEdGeWFXOHhEekFOQmdOVkJBY1RCazkwZEdGMwpZVEVRTUE0R0ExVUVDaE1IVTJodmNHbG1lVEVyTUNrR0ExVUVBeE1pVTJodmNHbG1lU0JRWVhsdFpXNTBJRkJoCmNuUnVaWElnUTBFZ1UzUmhaMmx1WnpBZUZ3MHlNVEE1TWpNeE1qRXpNemRhRncweU1qQTVNak14TWpFME1EZGEKTURVeE16QXhCZ05WQkFNVEtsTm9iM0JwWm5rZ1VHRjViV1Z1ZEhNZ1VHRnlkRzVsY2lBdElFWnZiMkpoY2lCVAphRzl3Y0dsdVp6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJPdUVKTjNRVlNuai9WbW95cElUClRiUzViVC9GRm1YMnh1ZEowMGNsTGllMTlPc3EwNVJjMFcvaDdXcXlZdzFuQS9aRXpZdnBzL3RaSWtCT2V5OXgKN2kralp6QmxNQTRHQTFVZER3RUIvd1FFQXdJRHVEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFkQmdOVgpIUTRFRmdRVWVJMVUrTGRaVDYrcUJOOFZSSzZ0ZFc0L2h2b3dId1lEVlIwakJCZ3dGb0FVa0lBWUNJY25HbklhCmEwejhEdHhFZy8vWExXWXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdhK0Vxd2RTTnRiZFZaM3IrN3lQb01GVmsKdWJ2L0hPMit6SC9CTXp0MjQ3c0NJQ1dISkNYdjc0enJaY2NTV0Z1UEtCQnR5d1RFYTY3UzlHS2xmTDQrcStrUQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==

You can use the body of the PEM-formatted public certificate with newlines removed as the HTTP request's Certificate header. For example, to remove newlines from the Base64-encoded body of the PEM-formatted public certificate, and output the result to STDOUT, you can run the following shell command:

Outputting a Base64-encoded

Shell command

cat certificate.pem | sed '1,1d;$ d;' | tr -d '\n'

Result

MIICDDCCAbOgAwIBAgIUR352YtBzbvY9dhcRHwn4lWyinaQwCgYIKoZIzj0EAwIwbzELMAkGA1UEBhMCQ0ExEDAOBgNVBAgTB09udGFyaW8xDzANBgNVBAcTBk90dGF3YTEQMA4GA1UEChMHU2hvcGlmeTErMCkGA1UEAxMiU2hvcGlmeSBQYXltZW50IFBhcnRuZXIgQ0EgU3RhZ2luZzAeFw0yMTA5MjMxMjEzMzdaFw0yMjA5MjMxMjE0MDdaMDUxMzAxBgNVBAMTKlNob3BpZnkgUGF5bWVudHMgUGFydG5lciAtIEZvb2JhciBTaG9wcGluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOuEJN3QVSnj/VmoypITTbS5bT/FFmX2xudJ00clLie19Osq05Rc0W/h7WqyYw1nA/ZEzYvps/tZIkBOey9x7i+jZzBlMA4GA1UdDwEB/wQEAwIDuDATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUeI1U+LdZT6+qBN8VRK6tdW4/hvowHwYDVR0jBBgwFoAUkIAYCIcnGnIaa0z8DtxEg//XLWYwCgYIKoZIzj0EAwIDRwAwRAIga+EqwdSNtbdVZ3r+7yPoMFVkubv/HO2+zH/BMzt247sCICWHJCXv74zrZccSWFuPKBBtywTEa67S9GKlfL4+q+kQ

Anchor to Retrieving encrypted credit card informationRetrieving encrypted credit card information

After finishing the steps above, you can then request encrypted credit card information and decrypt it when retrieved, using the Confirm an order endpoint.

Shopify uses the PEM-formatted public certificate included in the HTTP request headers to generate the encrypted credit card payload. If the request is successful, then the API response is returned in the following format:

JSON response format of the Confirm an order endpoint

{
"user": {
"uuid": "..."
},
"card": {
"uuid": "...",
"billingAddress": {
"addressLine": ["123 High Street"],
"city": "Mont Royal",
"country": "CA",
"dependentLocality": "",
"organization": "",
"phone": "+15145551212",
"postalCode": "123456",
"recipient": "Jane Doe",
"region": "QC",
"sortingCode": ""
},
"lastFourDigits": "3949",
"network": "VISA",
"type": "CREDIT",
"fingerprint": "v1:protected:fingerprint=",
"encryptedPayload": {
"encryptedMessage": "Jl0/NqxGDpbh6tL1h2NMIlAEciumZabIyMz0stfthjAsx7eerCtyGBDUEd383GNnQX+YKVIyo7QNjJOSA+0jbpX11BoIjcxy8G/jmMbC/0a+D08ZGyM555AXSeuwmh2wcBjdFVzEEA==",
"ephemeralPublicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENCkzCU6e8svEmMYSi37ZK+khnbSE\nPiK+edwCv4CTVsDaEeComLP8cPNWcTcdmDHunr2lqBZDO4f1NWUGRyA/fg==\n-----END PUBLIC KEY-----\n",
"tag": "UgumzY6zONdSfH7aDrEDWw=="
}
},
"shippingAddress": {
"addressLine": ["123 High Street"],
"city": "Mont Royal",
"country": "CA",
"dependentLocality": "",
"organization": "",
"phone": "+15145551212",
"postalCode": "123456",
"recipient": "Jane Doe",
"region": "QC",
"sortingCode": ""
}
}

The card.encryptedPayload blob contains the encrypted credit card information, along with all the data you need to decrypt it! It can be documented as follows:

AttributeTypeDescription
encryptedMessageBase64StringA base64-encoded, encrypted, JSON-formatted string of the credit card data.
ephemeralPublicKeyString (PEM)A plaintext, PEM-formatted string that is used to derive:

1. A shared secret for decrypting the encryptedMessage
2. An HMAC key used for verifying the tag

This attribute is labeled "ephemeral" as it will be unique to each encrypted payload.
tagBase64StringA base64-encoded HMAC tag used to verify the authenticity of the payload.

You now have all the tools needed to decrypt and verify the payment information. This will involve the following steps:

  1. Deriving a shared secret using the private key and the ephemeralPublicKey
  2. Expanding the shared secret to find the encryption key and the HMAC key
  3. Using the HMAC key to verify the encrypted payload authenticity
  4. Using the encryption key to decrypt the encryptedMessage

Let's run through what this will look like:

decryption.rb

decryption.rb

require 'openssl'
require 'base64'

### Setup ###

cipher = OpenSSL::Cipher.new('AES-256-CTR')
cipher_length_bytes = cipher.key_len

mac_digest = OpenSSL::Digest.new('SHA256')
mac_length_bytes = mac_digest.digest_length / 2

# Read the private key into a Ruby object
private_key = OpenSSL::PKey::EC.new(File.read('./private_key.pem'))

# Our values in the `encryptedPayload` in the API response
encrypted_message = 'Jl0/NqxGDpbh6tL1h2NMIlAEciumZabIyMz0stfthjAsx7eerCtyGBDUEd383GNnQX+YKVIyo7QNjJOSA+0jbpX11BoIjcxy8G/jmMbC/0a+D08ZGyM555AXSeuwmh2wcBjdFVzEEA=='
tag = 'UgumzY6zONdSfH7aDrEDWw=='

ephemeral_public_key = <<~EOC
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENCkzCU6e8svEmMYSi37ZK+khnbSE
PiK+edwCv4CTVsDaEeComLP8cPNWcTcdmDHunr2lqBZDO4f1NWUGRyA/fg==
-----END PUBLIC KEY-----
EOC

# Decoded data from the API response
encrypted_message_text = Base64.strict_decode64(encrypted_message)
mac = Base64.strict_decode64(tag)

### Decryption ###

# 1) Deriving the shared secret from the ephemeral public key
shared_secret = private_key.dh_compute_key(
OpenSSL::PKey::EC.new(ephemeral_public_key).public_key
)

# 2) Expanding the shared secret to find the encryption key and HMAC key. Salt and info should be empty.
key_pair = OpenSSL::KDF.hkdf(
shared_secret,
length: cipher_length_bytes + mac_length_bytes,
hash: 'SHA256',
salt: '',
info: ''
)

encryption_key = key_pair.byteslice(0, cipher_length_bytes)
hmac_key = key_pair.byteslice(-mac_length_bytes, mac_length_bytes)

# 3) Verify authenticity via the HMAC key
computed_mac = OpenSSL::HMAC.digest(
mac_digest,
hmac_key,
encrypted_message_text
).byteslice(0, mac_length_bytes)

raise OpenSSL::PKey::ECError, "Invalid Message Authenticaton Code" unless OpenSSL.secure_compare(computed_mac, mac)

# 4) Decrypt!
cipher.decrypt
cipher.iv = ("\x00" * 16).force_encoding(Encoding::BINARY)
cipher.key = encryption_key

decrypted_string = cipher.update(encrypted_message_text) + cipher.final

puts JSON.parse(decrypted_string)
# TODO

The last line should print out a JSON blob of the decrypted credit card info. Our decrypted example response should look as follows:

Sample Credit Card JSON

{
"type": "card",
"full_name": "Eren Yeager",
"pan": "4761209980003949",
"month": 9,
"year": 2024,
"brand": "visa"
}


Was this page helpful?