Payment method encryption
The Shop Pay Wallet will return plain (untokenized) credit card information for use in processing payments. This will consist of:
- Primary Account Number (PAN) (i.e. the card number)
- Full name on the card
- Expiry month and year
- The card brand
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.
Anchor to Encryption setupEncryption setup
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
CSR file
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
Then, if you introspect the CSR, you should see:
- The same
pub
value encoded into the CSR - 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
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
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
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
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
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 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)
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
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
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 theSubject 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
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
Anchor to Format 1Format 1
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
Example of a Base64-encoded public certificate
Anchor to Format 2Format 2
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
Result
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
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:
Attribute | Type | Description |
---|---|---|
encryptedMessage | Base64String | A base64-encoded, encrypted, JSON-formatted string of the credit card data. |
ephemeralPublicKey | String (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. |
tag | Base64String | A base64-encoded HMAC tag used to verify the authenticity of the payload. |
Anchor to DecryptingDecrypting
You now have all the tools needed to decrypt and verify the payment information. This will involve the following steps:
- Deriving a shared secret using the private key and the
ephemeralPublicKey
- Expanding the shared secret to find the encryption key and the HMAC key
- Using the HMAC key to verify the encrypted payload authenticity
- 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
- Getting started
- Authorization
- Shop Pay Wallet API reference
- Testing the integration
- Shop Pay Wallet ecosystem