How to read Apple VAS passes using an ACS WalletMate


Richard Grundy

Updated July 22, 2024 10:30

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:

  1. The ACR WalletMate feature list
  2. Where to buy a WalletMate
  3. How to download drivers for using the WalletMate
  4. Issuing a pass that works with the WalletMate
  5. Installing psycard to read data
  6. Connecting to the reader
  7. Getting the NFC message in encrypted format
  8. 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:

drivers-screenshot

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:

reader-list

👀 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:

session-start

Here it is so you can copy it:

CLAINSP1P2LEDATA
FFC20000028100

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:

CLAINSP1P2LEDATA
FFC200000200A404000A4F53452E5641532E303100

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:

pass-type-id

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:

CLAINSP1P2LEDATA
80CA0101369F220201009F252036CEA63BDA4BE5E17F99CAEF06860507E9D8F021C2095A47668ACEB00DE3ADB29F2804C5266B6E9F260400000002

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:

private-key-download

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

Was this article helpful?
Yes No