How to read Apple VAS passes using an ACS WalletMate
Updated December 17, 2024 08:43
Overview
If you have an ACS WalletMate, you may be wondering how to use it to read Apple passes. In this article, we'll be going over the steps to do just that:
- The ACR WalletMate feature list
- Where to buy a WalletMate
- How to download drivers for using the WalletMate
- Issuing a pass that works with the WalletMate
- Installing psycard to read data
- Connecting to the reader
- Getting the NFC message in encrypted format
- Decrypting the NFC message
👀 This guide also assumes you are using LibreSSL or OpenSSL on *nix systems.
The WalletMate
ACS makes many different smart card products, but we're going to focus on a USB connected, VAS compliant reader called the WalletMate.
It supports ISO 14443 Type A and B cards, MIFARE®, FeliCa, and ISO 18092–compliant NFC tags which allow it to read Apple VAS passes and Google SmartTap passes. If you want to buy them in bulk, you can do so here.
Since I only needed one, I bought mine from GoToTags.
Downloading the drivers
Next, we'll need the drivers that allow our computer to connect to the reader. You can download the drivers for your specific OS from the ACS website:
Once you download them, be sure to install them! If you do not install them, then your script (down below) will not work. This will require you to restart your computer if you're using a Mac.
Issuing a pass
Next you'll need to issue an Apple pass that has NFC data. We highly recommend using PassNinja for this because it makes it ridiculously easy. You don't need to generate any private keys if you do not want too, you do not need to figure out how to create compressed public keys and more.
However, if you're building your own passes, you'll need to populate the nfc
key with an object literal that has the message
and encryptionPublicKey
properly populated:
{
"logoText": "Your Company Name",
"labelColor": "rgb(0, 0, 0)",
"nfc": {
"message": "[whatever-64-char-message-you-want]",
"encryptionPublicKey": "MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgADkGZpDGxw4yTToVFCVkCa7whytzCcUqkGZh9uBC5LbBw="
}
}
If you're building your own passes please keep the private key that is tied to your encryptionPublicKey
handy. We'll need it later.
Installing pyscard and other dependencies
For this tutorial you'll need the pyscard python package. You only need to run one of these:
python3 -m pip install pyscard
pip3 install pyscard
We also need a cryptography package. Again, you only need to run one of these:
python3 -m pip install cryptography
pip3 install cryptography
As mentioned above, we also need you to have LibreSSL or OpenSSL installed:
brew install openssl
brew install libressl
Connecting to the reader
In order to connect to the reader, we're going to need access to the ACS reader manual. They don't have it under the ACR WalletMate product page, but rather the ACR1252U product page. We'll use psycard
to connect and see a list of the available readers. Put this code in a file called connect.py
:
from smartcard.scard import (
SCardGetErrorMessage,
SCARD_SCOPE_USER,
SCARD_S_SUCCESS,
SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0,
SCARD_PROTOCOL_T1,
SCARD_CTL_CODE,
SCardEstablishContext,
SCardListReaders,
SCardConnect,
SCardControl
)
def connect_to_reader():
hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
assert hresult == SCARD_S_SUCCESS
hresult, readers = SCardListReaders(hcontext, [])
assert len(readers) > 0
print(readers)
reader = readers[0]
hresult, hcard, dwActiveProtocol = SCardConnect(hcontext, reader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T1)
return reader, hcard
connect_to_reader()
When you run this, you'll see something like the following:
👀 if you look closely, you'll notice that there are two readers listed. This is because one is for the SAM (Secure Access Module) and the other is for the PICC (Proximity Integrated Circuit Card aka NFC tag). This is not listed anywhere in their manual, but it is in some obscure github issue on their driver repo.
We're going to use the reader in position 0 (zero) because for us it's the one that reads NFC tags.
Starting a session
Next we need to start a "transparent session", this will allow us to put the reader into "VAS reading mode", so to speak. A bit more technically, it allows us to use the reader as a proxy and issue APDUs directly to the NFC tag that is in field. We got the APDUs for "session starting" from the ACS manual:
Here it is so you can copy it:
CLA | INS | P1 | P2 | LE | DATA |
FF | C2 | 00 | 00 | 02 | 8100 |
And here is a function to start a session:
def send_apdu(hcard, command, fmt):
hresult, response = SCardTransmit(hcard, SCARD_PCI_T1, command)
if hresult != SCARD_S_SUCCESS:
print(SCardGetErrorMessage(hresult))
exit()
if fmt == "str":
return ''.join(map(lambda x: chr(x), response))
elif fmt == "hex":
return ''.join(map(lambda x: f'{x:02x}', response)).replace('0x', '')
else:
return response
def start_session(hcard, fmt):
print(">> Starting session...")
res = send_apdu(hcard, [0xFF, 0xC2, 0x00, 0x00, 0x02, 0x81, 0x00], fmt)
print("== Session started! ✅")
return res
Starting the antenna
We'll need to spin up the antenna as well!
def start_antenna(hcard, fmt):
print(">> Starting antenna...")
res = send_apdu(hcard, [0xFF, 0xC2, 0x00, 0x00, 0x02, 0x84, 0x00], fmt)
print("== Antenna started! ✅")
return res
Starting VAS reading with SELECT
Next we're going to need a function to tell the Apple pass we want to SELECT it. This is not documented anywhere officially, but we found this very awesome VAS reverse engineering guide from Grayson Martin that tells us the standard ISO7816 select command --with AID equal to the ASCII encoding of "OSE.VAS.01"-- will get us going!
Here is the APDU:
CLA | INS | P1 | P2 | LE | DATA |
FF | C2 | 00 | 00 | 02 | 00A404000A4F53452E5641532E303100 |
Here is the function:
def start_vas(hcard, fmt):
print(">> Starting VAS SELECT...")
select_cmd = [0x00, 0xA4, 0x04, 0x00, 0x0A, 0x4F, 0x53, 0x45, 0x2E, 0x56, 0x41, 0x53, 0x2E, 0x30, 0x31, 0x00]
res = send_apdu(hcard, select_cmd, fmt)
print("== VAS selected! ✅")
return res
Pulling pass data with GET VAS DATA
Now that the tag is SELECTed, we can ask it for its VAS data. This is a highly involved process that is well documented in the breakdown from Grayson. We're going to use the pass type ID that is in our passninja.com dashboard:
If you're not sure how to create a pass type ID, you can just use PassNinja, or follow this tutorial.
Anyway, here is our Command APDU for GET VAS DATA:
CLA | INS | P1 | P2 | LE | DATA |
80 | CA | 01 | 01 | 36 | 9F220201009F252036CEA63BDA4BE5E17F99CAEF06860507E9D8F021C2095A47668ACEB00DE3ADB29F2804C5266B6E9F260400000002 |
Now in code:
def get_vas_data(hcard, fmt):
print(">> Starting GET VAS DATA...")
getvas_cmd = [0x80, 0xCA, 0x01, 0x01, 0x36, 0x9F, 0x22, 0x02, 0x01, 0x00, 0x9F, 0x25, 0x20, 0x36, 0xCE, 0xA6, 0x3B, 0xDA, 0x4B, 0xE5, 0xE1, 0x7F, 0x99, 0xCA, 0xEF, 0x06, 0x86, 0x05, 0x07, 0xE9, 0xD8, 0xF0, 0x21, 0xC2, 0x09, 0x5A, 0x47, 0x66, 0x8A, 0xCE, 0xB0, 0x0D, 0xE3, 0xAD, 0xB2, 0x9F, 0x28, 0x04, 0xC5, 0x26, 0x6B, 0x6E, 0x9F, 0x26, 0x04, 0x00, 0x00, 0x00, 0x02]
res = send_apdu(hcard, getvas_cmd, fmt)
print("== VAS DATA COLLECTED! ✅")
return res
This does not return us a human readable value. This is because the data is still encrypted. We'll need to decrypt this payload using our private key.
Decrypting the NFC message
The mechanics of decryption are well explained in the document we linked to above. You'll need the private key we referenced in the section titled "Issuing a pass". If you're a PassNinja customer, you can just download this from the config tab of your pass template:
Now, here are the functions to decrypt the payload:
PRIVATE_KEY_PEM = """-----BEGIN PRIVATE KEY-----
YOUR-PRIVATE-KEY-HERE
-----END PRIVATE KEY-----"""
PUBLIC_KEY_ASN_HEADER = bytes.fromhex(
"3039301306072a8648ce3d020106082a8648ce3d030107032200"
)
def generate_shared_info(pass_identifier: str):
return bytes([
0x0D,
*"id-aes256-GCM".encode("ascii"),
*"ApplePay encrypted VAS data".encode("ascii"),
*hashlib.sha256(pass_identifier.encode("ascii")).digest()
])
def decrypt_vas_data(cryptogram: bytearray, pass_identifier: str):
device_key_id = cryptogram[:4]
device_public_key_body = cryptogram[4: 32 + 4]
device_encrypted_data = cryptogram[36:]
reader_private_key = load_pem_private_key(PRIVATE_KEY_PEM.encode(), None, default_backend())
for sign in (0x02, 0x03):
try:
device_public_key = load_der_public_key(
PUBLIC_KEY_ASN_HEADER + bytearray([sign]) + device_public_key_body
)
shared_key = reader_private_key.exchange(ec.ECDH(), device_public_key)
shared_info = generate_shared_info(pass_identifier)
derived_key = X963KDF(
algorithm=hashes.SHA256(),
length=32,
sharedinfo=shared_info,
).derive(shared_key)
device_data = AESGCM(derived_key).decrypt(b'\x00' * 16, bytes(device_encrypted_data), b'')
timestamp = datetime(year=2001, month=1, day=1) + timedelta(seconds=int.from_bytes(device_data[:4], "big"))
payload = device_data[4:].decode("utf-8")
return timestamp, payload
except Exception as e:
pass
else:
raise Exception("Could not decrypt data")
Ending a session
Finally, we should end the transparent session so that any additional APDUs are sent to the reader hardware itself and not the Apple pass. Here is a function for that:
def end_session(hcard, fmt):
print(">> Ending session...")
res = send_apdu(hcard, [0xFF, 0xC2, 0x00, 0x00, 0x02, 0x82, 0x00], fmt)
print("== Session ended! ✅")
return res
Putting it all together
Now all together in one big file, all of the necessary imports:
import time
import hashlib
from datetime import datetime, timedelta
from smartcard.CardMonitoring import CardMonitor, CardObserver
from smartcard.System import readers
from smartcard.util import toHexString
from smartcard.CardConnection import CardConnection
from smartcard.scard import SCardGetErrorMessage, SCARD_SHARE_DIRECT, SCARD_SCOPE_USER, SCARD_S_SUCCESS, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0, SCARD_PROTOCOL_T1, SCARD_CTL_CODE, SCardEstablishContext, SCardListReaders, SCardConnect, SCardControl, SCardTransmit, SCARD_PCI_T0, SCARD_PCI_T1
from smartcard.Exceptions import CardConnectionException
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_der_public_key, load_pem_private_key
PRIVATE_KEY_PEM = """-----BEGIN PRIVATE KEY-----
YOUR-PRIVATE-KEY-HERE
-----END PRIVATE KEY-----"""
PUBLIC_KEY_ASN_HEADER = bytes.fromhex(
"3039301306072a8648ce3d020106082a8648ce3d030107032200"
)
def is_valid_ec_key(strang):
compressed_ephemeral_public_key = bytes(strang[4:36])
compressed_ephemeral_public_key = b'\x02' + compressed_ephemeral_public_key
try:
# Attempting to load the public key
public_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), compressed_ephemeral_public_key)
return("The public key is valid.")
except ValueError:
return("The public key is not valid.")
except Exception as e:
return(f"An error occurred: {e}")
def is_valid_pub_key(device_public_key_body):
device_public_key_body = device_public_key_body[4:36]
try:
device_public_key = load_der_public_key(
PUBLIC_KEY_ASN_HEADER + bytearray([0x02]) + device_public_key_body
)
return("The public key is valid.")
except Exception as e:
return(f"An error occurred: {e}")
def connect_to_reader():
hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
assert hresult == SCARD_S_SUCCESS
hresult, readers = SCardListReaders(hcontext, [])
assert len(readers) > 0
print(readers)
reader = readers[0]
hresult, hcard, dwActiveProtocol = SCardConnect(hcontext, reader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T1)
return reader, hcard
def send_apdu(hcard, command, fmt):
hresult, response = SCardTransmit(hcard, SCARD_PCI_T1, command)
if hresult != SCARD_S_SUCCESS:
print(SCardGetErrorMessage(hresult))
exit()
if fmt == "str":
return ''.join(map(lambda x: chr(x), response))
elif fmt == "hex":
return ''.join(map(lambda x: f'{x:02x}', response)).replace('0x', '')
else:
return response
def start_session(hcard, fmt):
print("====> Starting session...")
res = send_apdu(hcard, [0xFF, 0xC2, 0x00, 0x00, 0x02, 0x81, 0x00], fmt)
print("---------session started-----------")
return res
def start_antenna(hcard, fmt):
print("====> Starting session...")
res = send_apdu(hcard, [0xFF, 0xC2, 0x00, 0x00, 0x02, 0x84, 0x00], fmt)
print("---------session started-----------")
return res
def start_vas(hcard, fmt):
print("====> Starting VAS select...")
select_cmd = [0x00, 0xA4, 0x04, 0x00, 0x0A, 0x4F, 0x53, 0x45, 0x2E, 0x56, 0x41, 0x53, 0x2E, 0x30, 0x31, 0x00]
res = send_apdu(hcard, select_cmd, fmt)
print("---------VAS selected-----------")
return res
def get_vas_data(hcard, fmt):
print("====> Starting VAS data collection...")
getvas_cmd = [0x80, 0xCA, 0x01, 0x01, 0x36, 0x9F, 0x22, 0x02, 0x01, 0x00, 0x9F, 0x25, 0x20, 0x36, 0xCE, 0xA6, 0x3B, 0xDA, 0x4B, 0xE5, 0xE1, 0x7F, 0x99, 0xCA, 0xEF, 0x06, 0x86, 0x05, 0x07, 0xE9, 0xD8, 0xF0, 0x21, 0xC2, 0x09, 0x5A, 0x47, 0x66, 0x8A, 0xCE, 0xB0, 0x0D, 0xE3, 0xAD, 0xB2, 0x9F, 0x28, 0x04, 0xC5, 0x26, 0x6B, 0x6E, 0x9F, 0x26, 0x04, 0x00, 0x00, 0x00, 0x02]
res = send_apdu(hcard, getvas_cmd, fmt)
print("---------VAS data collected-----------")
return res
def end_session(hcard, fmt):
print("====> Ending session...")
res = send_apdu(hcard, [0xFF, 0xC2, 0x00, 0x00, 0x02, 0x82, 0x00], fmt)
print("---------session ended-----------")
return res
def generate_shared_info(pass_identifier: str):
return bytes([
0x0D,
*"id-aes256-GCM".encode("ascii"),
*"ApplePay encrypted VAS data".encode("ascii"),
*hashlib.sha256(pass_identifier.encode("ascii")).digest()
])
def decrypt_vas_data(cryptogram: bytearray, pass_identifier: str):
device_key_id = cryptogram[:4]
device_public_key_body = cryptogram[4: 32 + 4]
device_encrypted_data = cryptogram[36:]
reader_private_key = load_pem_private_key(PRIVATE_KEY_PEM.encode(), None, default_backend())
for sign in (0x02, 0x03):
try:
device_public_key = load_der_public_key(
PUBLIC_KEY_ASN_HEADER + bytearray([sign]) + device_public_key_body
)
shared_key = reader_private_key.exchange(ec.ECDH(), device_public_key)
shared_info = generate_shared_info(pass_identifier)
derived_key = X963KDF(
algorithm=hashes.SHA256(),
length=32,
sharedinfo=shared_info,
).derive(shared_key)
device_data = AESGCM(derived_key).decrypt(b'\x00' * 16, bytes(device_encrypted_data), b'')
timestamp = datetime(year=2001, month=1, day=1) + timedelta(seconds=int.from_bytes(device_data[:4], "big"))
payload = device_data[4:].decode("utf-8")
return timestamp, payload
except Exception as e:
pass
else:
raise Exception("Could not decrypt data")
reader, hcard = connect_to_reader()
start_session(hcard, "raw")
start_antenna(hcard, "raw")
start_vas(hcard, "str")
try:
vas_raw_payload = get_vas_data(hcard, "hex")
if vas_raw_payload[-4:] == "9000":
vas_shaped_payload = bytearray.fromhex(vas_raw_payload[16:-4])
ts, data = decrypt_vas_data(vas_shaped_payload, "pass.com.passninja.rails.generic")
print(f'NFC payload: {data}')
else:
print(f'SW not 9000. Instead: {vas_raw_payload[16:-4]}')
except Exception as e:
print("ERROR!!")
print(e)
finally:
end_session(hcard, "raw")
Conclusion
We covered a whole lot in this tutorial. You now have knowledge on ACS WalletMate, the VAS protocol, how to install pyscard, how to use pyscard, what transparent sessions are, and how to read the payload from your Apple pass and decrypt it.
If you have any feedback on this article, let us know! Email content@passninja.com
More articles focused on Hardware
The VTAP series of NFC readers from DotOrigin are popular because of their ease of use and broad ...
How To Build An Nfc Car KeyThis guide will show you how to replace your car keys with an NFC-enabled pass in your Apple or G...
How To Configure A Dot Origin Vtap100 Nfc ReaderThis guide will cover the process of setting up a VAS reader to detect and verify Apple and Googl...
How To Control Any Device With A Relay And Raspberry PiThis guide covers how to control electrical equipment like an appliance or battery-powered device...
How To Control Vtap From Background (Windows)The VTAP series of NFC readers from DotOrigin are popular because of their ease of use and broad ...
How To Read Nfc Data From Vtap Via IpadIn this guide, we'll show you how to create an iPad app that is capable of reading NFC data from ...
How To Configure A Socket Mobile S550 Bluetooth Nfc ReaderThis guide will cover the process of setting up a VAS compatible bluetooth enabled NFC reader to ...