Talk to your Credit Card part 1: select PPSE (Paypass Payment System Environment)

AndroidCrypto
6 min readApr 12, 2023

--

This is the first step of the article series about reading a Credit Card and retrieve the card number and it’s expiration date.

The previous article described the general workflow and basic elements of the app, now we are implementing the first step.

Note: I tested my app with more than 20 payment cards (American Express, VisaCard debit/credit, MasterCard debit/credit, (German) GiroCards maestro/VPay) and all of them contained the PPSE. Recording to the EMV specifications it is not guaranteed that a card has a PPSE and in that case the terminal should skip the reading of PPSE and proceed to step 3 (select an application). The terminal shall in that case try a list of “well known application identifiers” to select an application on the card. My app doesn’t support this procedure and will terminate the reading process. This may happen e.g. when reading a card from Union Pay, Diners, JCB or other cards uncommon in Germany, sorry.

Step 1: ask the card which applications are available on the card (“select PPSE”)

Each NFC tag that is connected using the IsoDep-class has it’s own “wording” that is documented in a specification. Fortunately the specifications for EMV cards are published and are readable without any access limitations. In the repository for the Basis app you find the most important specifications for download.

Our journey to read the Credit Card beginns with a simple string:

byte[] PPSE = "2PAY.SYS.DDF01".getBytes(StandardCharsets.UTF_8);

This is the entry point to our Credit Card and just needs to get send to the Credit Card using a transceive-command. The card will answer this command with a response that gives us important informations about how to go the next steps. Here is the code for that step:

byte[] PPSE = "2PAY.SYS.DDF01".getBytes(StandardCharsets.UTF_8); // PPSE
byte[] selectPpseCommand = selectApdu(PPSE);
byte[] selectPpseResponse = nfc.transceive(selectPpseCommand);

The “selectApdu” combines the PPSE byte array with a command sequence that includes a length field. The details of the command can be found in the specifications:

private byte[] selectApdu(@NonNull byte[] data) {
byte[] commandApdu = new byte[6 + data.length];
commandApdu[0] = (byte) 0x00; // CLA
commandApdu[1] = (byte) 0xA4; // INS
commandApdu[2] = (byte) 0x04; // P1
commandApdu[3] = (byte) 0x00; // P2
commandApdu[4] = (byte) (data.length & 0x0FF); // Lc
System.arraycopy(data, 0, commandApdu, 5, data.length);
commandApdu[commandApdu.length - 1] = (byte) 0x00; // Le
return commandApdu;
}

A typical printout of the command and answer could look like this:

01 select PPSE command  length 20 data: 00a404000e325041592e5359532e444446303100
01 select PPSE response length 64 data: 6f3c840e325041592e5359532e4444463031a52abf0c2761254f07a000000004101050104465626974204d6173746572436172648701019f0a04000101019000
... deleted some data to see the end:
01 select PPSE response length 64 data: 6f3c ... 019000

The most important information is at the end of the answer: there is a trailing “9000” that indicates that the command was accepted by the card and the data is valid. Follow the link to get the complete list of APDU commands.

For that reason there is a simple “checkResponse” method available that checks for the trailing “9000” and gives us the data back without the trailer. If the response is not successful the method will return a NULL value:

    private byte[] checkResponse(@NonNull byte[] data) {
// simple sanity check
if (data.length < 5) {
return null;
} // not ok
int status = ((0xff & data[data.length - 2]) << 8) | (0xff & data[data.length - 1]);
if (status != 0x9000) {
return null;
} else {
return Arrays.copyOfRange(data, 0, data.length - 2);
}
}

The card answered with “Command successfully executed (OK).” but what can we do with this information ?

For a first analysis we copy and paste the response to the website https://emvlab.org/tlvutils/ and get this result (this link gives you the same information without copy & paste):

6F File Control Information (FCI) Template
84 Dedicated File (DF) Name
325041592E5359532E4444463031
A5 File Control Information (FCI) Proprietary Template
BF0C File Control Information (FCI) Issuer Discretionary Data
61 Application Template
4F Application Identifier (AID) – card
A0000000041010
50 Application Label
D e b i t M a s t e r C a r d
87 Application Priority Indicator
01
9F0A Unknown tag
00010101

Wow — I didn’t expected that there is so much information in a 64 bytes long card response. To be honest: most of the text information is added by the website for a better human reading. Where does the website get the additional information from ? The answer is simple: from the specifications as well, but to retrieve the information in that form we need to dig deeper in the details.

The most important information is: Most response data is encoded in BER-TLV sequences. This is the short version of “Basic Encoding Rules — Text Length Values”; for an explanation I’m starting at the end:

TLV decribes that the data is encapsulated in the form:

  1. Tag that is the “identifier” for the data
  2. Length: how many bytes will follow
  3. Value: the data that are available for this tag

Let’s start a short manual reading of our response data — but not starting at the beginning:

..254f07a000000004101050104465626974204d6173746572436172648701
4f is the tag
07 is the length (hex 0x07 = decimal 07)
a0000000041010 is the data following
50 is the tag
10 is the length (hex 0x10 = decimal 16)
4465626974204d617374657243617264 ... data

This sounds simple but that won’t work for the beginning of our data and reason for that is that the first tag “60” is a “constructed” tag, means it has not only data but a child or child tree following. The second issue arises when having a look at the tag “BF0C”:

BF0C File Control Information (FCI) Issuer Discretionary Data

This is a 2 bytes long tag and the byte after this tag gives the length. The reason for both hick-ups is simple: our answer is not only of a TLV- but of a BER-TLV structure and we do need a lot of coding to get the data for the following steps by our own. The second thing is: we also need to have a table available with all names of the tags — uh, a lot of work.

For that reason I’m including two 3rd. party libraries into my app that all do the heavy jobs for us:

a) for easy parsing BER-TLV encoded data I’m using a BER-TLV encoder available here: https://github.com/evsinev/ber-tlv

b) for nice printouts and other BER-TLV related tasks I’m using the library “EMV-NFC-Paycard-Enrollment” available here: https://github.com/devnied/EMV-NFC-Paycard-Enrollment. Please note that this library provides a full working EMV card reader but as all reading tasks are “under the hood” I do the tasks on my own to show every step in detail.

With the help it is very easy for us (a “one line of code”) to get a nice output of our data elements:

writeToUiAppend(prettyPrintDataToString(selectPpseResponse));
------------------------------------
6F 3C -- File Control Information (FCI) Template
84 0E -- Dedicated File (DF) Name
32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 (BINARY)
A5 2A -- File Control Information (FCI) Proprietary Template
BF 0C 27 -- File Control Information (FCI) Issuer Discretionary Data
61 25 -- Application Template
4F 07 -- Application Identifier (AID) - card
A0 00 00 00 04 10 10 (BINARY)
50 10 -- Application Label
44 65 62 69 74 20 4D 61 73 74 65 72 43 61 72 64 (=Debit MasterCard)
87 01 -- Application Priority Indicator
01 (BINARY)
9F 0A 04 -- [UNKNOWN TAG]
00 01 01 01 (BINARY)
90 00 -- Command successfully executed (OK)
------------------------------------

Btw: at this point we got the information that the card is a “Mastercard debit” card but: there are a lot of “optional” elements and this information is available on on some cards I read, so don’t rely that all of the data is available on every card!

I added this code in the MainActivity.java class:

public void onTagDiscovered(Tag tag) {
...
printStepHeader(1, "select PPSE");
byte[] PPSE = "2PAY.SYS.DDF01".getBytes(StandardCharsets.UTF_8); // PPSE
byte[] selectPpseCommand = selectApdu(PPSE);
byte[] selectPpseResponse = nfc.transceive(selectPpseCommand);
writeToUiAppend("01 select PPSE command length " + selectPpseCommand.length + " data: " + bytesToHexNpe(selectPpseCommand));
writeToUiAppend("01 select PPSE response length " + selectPpseResponse.length + " data: " + bytesToHexNpe(selectPpseResponse));
writeToUiAppend(etData, "01 select PPSE completed");
writeToUiAppend(prettyPrintDataToString(selectPpseResponse));

byte[] selectPpseResponseOk = checkResponse(selectPpseResponse);
// proceed only when te do have a positive read result = 0x'9000' at the end of response data
if (selectPpseResponseOk != null) {

} else {
// if (isoDepInTechList) {
writeToUiAppend("The discovered NFC tag does not have an IsoDep interface.");
}
...

and later:
/**
* build a select apdu command
*
* @param data
* @return
*/
private byte[] selectApdu(@NonNull byte[] data) {
byte[] commandApdu = new byte[6 + data.length];
commandApdu[0] = (byte) 0x00; // CLA
commandApdu[1] = (byte) 0xA4; // INS
commandApdu[2] = (byte) 0x04; // P1
commandApdu[3] = (byte) 0x00; // P2
commandApdu[4] = (byte) (data.length & 0x0FF); // Lc
System.arraycopy(data, 0, commandApdu, 5, data.length);
commandApdu[commandApdu.length - 1] = (byte) 0x00; // Le
return commandApdu;
}

Find the full code of the app in my GitHub repository TalkToYourCreditCardPart1: TalkToYourCreditCardPart1

Next step 2 is: analyze the card’s response and identify one or more of the application number or application id (“AID”):

--

--