Enhanced Contactless Polling (ECP) is a proprietary extension to the ISO/IEC 14443 (A/B) standard developed by Apple.
It defines a custom data frame that a contactless reader has to transmit during the polling sequence, providing an end device with contextual info about the reader field, allowing it to decide if it wants to resolve routing to a particular applet or system feature even before any back-and-forth communication starts.
This extension:
- Helps to make sure that the end device will only start communication with the reader if it has something useful to do with it, avoiding error beeps and card clash;
- Increases privacy and security as it complicates brute force scanning for available passes on the device in a single tap.
- Allows automatic usage of non ISO7816-compliant passes:
- DESFire in native mode and on card-level instead of app-level;
- Passes without application ID (Mifare Plus in some cases).
- Helps with conflict resolution when there are multiple passes with the same ISO7816 AID:
- Allows the device to differentiate between GymKit and ISO18013 even though both use NDEF AID for BLE handover.
- Resolves routing issues between access credentials from the same system manufacturer (HID, AssaAbloy, WaveLynx, Brivo, Kisi, etc.) even if they use the same AIDs and the underlying technology does not support subcredentials at application level.
- May serve as a form of NFC DRM, requiring reader manufacturers to pay licensing fees in order to be able to use this feature and provide a better experience for Apple users.
ECP is also sometimes referred to as Enhanced Contactless Protocol. For explanation, look into extras section
Express mode for most passes (apart from FeliCa and GenericA) is implemented using ECP. That includes:
- Credit cards (For transit fallback);
- Transit cards;
- Access passes and keys:
- University;
- Office badges;
- Venue (Theme parks);
- Apartment (Multi-family homes);
- Hotel;
- Car;
- Home.
Other features use ECP as well:
- Value Added Services (VAS):
Allows the reader to select the VAS applet and try to get a pass in advance (although failing to do so), causing a pass to appear on a screen for authentication or under a payment card if one is selected.

- GymKit:
Makes Apple Watch act as an NDEF tag for BLE handover in order to connect to supported gym equipment.

- Identity:
Makes an Apple device act as an NDEF tag for BLE handover in order to connect to an ISO18013 verifier. - CarKey Setup:
Tells the device what car brand it is, causing a car key setup popup to appear on a screen. - Field ignore:
Makes Apple devices not react (where "react" means displaying a default payment card) to a field generated by other Apple devices. - AirDrop:
Replaces field ignore in iOS 17 for background reading, used to negotiate an AirDrop session. NameDrop is a special case of AirDrop and it triggers a warp animation.
Reader side:
- Can be implemented in software on most devices, provided that low-level access to NFC hardware is available. In some cases, it is required to re-implement parts of the protocol stack in the software when doing so.
- HALs/Libraries for most popular chips contain separate confidential versions that include ECP support and are given to approved partners only, but a homebrew solution is easy to implement.
- Proof of concept was successfully tested using PN532, PN5180, ST25R3916(B), MFRC522 chips and PCSCV2 readers; For information about those chips and how ECP can be implemented on them, visit the Examples page.
- iOS has special reader APIs that make the device emit specific ECP frames:
- NFCVASReaderSession, PaymentCardReaderSession for VAS;
- MobileDocumentReaderSession for Identity;
- When using other derivatives of NFCReaderSession, the device emits the Ignore frame so that other Apple devices don't react to it.
- Some Android-based handheld reader manufacturers have implemented this feature in their software.
Device side:
- Implemented using a customized CRS applet;
- Can be implemented on chips that allow reading raw frames in emulation mode even before selection;
- Can be reproduced on select mobile handsets running the Android 15 operating system, with the introduction of Observe Mode, which allows Android devices to look at the polling frames sent by the contactless reader even before starting communication.
Upon entering a loop, the device does not answer to the first polling frame it sees, instead opting to wait and see what other technologies the field polls for, allowing it to make a fully informed decision on what applet or feature to trigger later.
When the device makes a decision, it is mostly, although not in all cases (excluding keys) signified by a card image appearing along with a spinner.
Even though ECP is sent during the polling loop, the device does not answer to it directly. Instead, it responds to a polling frame related to the technology of the pass that the device had decided to use.
When the device enters the loop initially:
- In case of a full polling loop (A,B,F) it waits through one full iteration before making a decision on what applet to select:
(ENTRY) -> A -> ECP_A -> B -> ECP_B -> F -> (DECISION) -> A -> (RESPONSE)
A -> ECP_A -> (ENTRY) -> B -> ECP_B -> F -> A -> ECP_A -> (DECISION) -> B -> (RESPONSE)
- In case of partial or weirdly-ordered polling loop, behavior is different. For example:
(ENTRY) -> A -> ECP_A -> A -> ECP_A -> (DECISION) -> A -> (RESPONSE)
(ENTRY) -> F -> B -> ECP_B -> A -> F -> B -> (DECISION) -> ECP_B -> A -> (RESPONSE)
(ENTRY) -> A -> ECP_B -> F -> A -> ECP_B -> (DECISION) -> F -> A -> (RESPONSE)
(ENTRY) -> F -> F -> F -> (DECISION) -> F -> (RESPONSE)
(ENTRY) -> A -> A -> A -> (DECISION)
(ENTRY) -> A -> ECP_A -> F -> A -> ECP_A -> F -> (DECISION) -> A -> (RESPONSE)
Characters A, B, and F were used in examples as a shorthand for full polling frame names: WUPA, WUPB, SENSF_REQ respectively. ECP frame has different values depending on the use case. The _A/B suffix refers to the modulation used.
Tests were conducted using very big intervals between polling frames. IRL if polling is faster device might respond after more frames than shown, presumably because of internal processing delay.
In conclusion, if the reader is polling for:
- 1 technology, the decision is made after the third poll, the response is given on the fourth;
- 2 technologies, the decision is made after the second polling loop, while the response is given on the third.
- 3 technologies, the decision is made after the first loop, the response is given on the second.
Apple devices seem to have a grace period after leaving an NFC field, where if a device re-enters the field in time, it will still consider it to be the same field.
It was done in order to better support express mode with devices that turn off the NFC field in between polling iterations in order to conserve energy.
This way, an end device wouldn't think that it entered a new field on each polling iteration, which would've prevented express mode from being triggered, as it needs at least two loops for selection.
According to tests, the grace period duration depends on what pass types are enabled for express mode on the device. The grace period duration setting seems to be separate for each NFC technology, with the worst/maximum value taken from enabled pass categories:
- ECP (NFC-A/B) + GENERIC-A (NFC-A):
- (EMV) Payment card:
300ms; - Transit card:
750ms.
- (EMV) Payment card:
- FeliCa (NFC-F):
- Transit card:
750ms;
- Transit card:
For improved stability and increased polling performance, it is advised to poll in the 100-250 ms range or faster, keeping in mind the field activity duty cycle.
Even though NFC polling can be really fast, it still takes some time for a device to analyze polling frames and make a decision on which routing to activate in order to begin communication.
During tests, the following delays have been measured:
- ECP (NFC-A/B):
- PC + PN532:
100ms; - Proxmark3:
1031640 / 13560000 * 1000 ~= 76ms or ~45 polling iterations;
- PC + PN532:
- FeliCa (NFC-F):
- PC + PN532:
50ms. - Proxmark3:
547584 / 13560000 * 1000 ~= 40ms or 3 polling iterations;
- PC + PN532:
- GENERIC-A (NFC-A):
- Proxmark3, no delay:
1042912 / 13560000 * 1000 ~= 77ms or ~150 polling iterations; - Proxmark3,
5ms delay:979680 / 13560000 * 1000 ~= 72ms or ~12 polling iterations;
- Proxmark3, no delay:
It's important to understand that the hardware used for measuring might have some operational delay itself.
PC + PN532 method yields a bigger measurement error as it runs on a non-real-time system.
Proxmark3 method gives more accurate results as the measurement is done on a separate device, which is intended specifically for NFC analysis.
As a result of the tests, we have the following findings:
- FeliCa has from 50% to 100% less processing delay in comparison to NFC-A/B, which is quite interesting.
- One assumption is that additional delay may be caused by ECP part of the resolution stack inside of CRS, which does not impact FeliCa resolution, as it has a native system-code-based method of resolving applets for express mode.
- The other assumption is that this delay is arbitrary, and is used to prevent situations when communication is started "just as" the device barely enters the field, when signal strength is poor and long commands can be interrupted.
- With NFC-A, if the polling frame is sent really fast without any arbitrary delays, the device might actually respond a little bit slower (up to
82ms).
The sweet spot for response speed is about5ms guard time between polling attempts, which can reduce delay down to about70ms.
In normal deployments, a reader polls only for one express-mode technology at a time, but if a reader is polling for multiple express technology qualifiers, the following technology priority will be applied:
- ECP (
ecp):- Primary (
ecp.2.tci); - EMV Fallback (
ecp.2.open_loop).
- Primary (
- FeliCa (
felica.*):- CJRC (
suica) / Octopus (cathay) - chosen randomly depending on which frame is seen first.
- CJRC (
- GenericA (
generic.type_a):- T-Money Korea / T-Union China - only one can be enabled at a time.
QuickMode/SinglePoll(quickmode) - deprecated precursor togeneric.type_awhich wasn't compatible with ECP.
There could be more valid express mode qualifiers available, but they have to be verified to be used on real passes before being listed here. If you have any info, feel free to create a PR.
- If polling for both ECP and FeliCa, the device will sometimes display the FeliCa pass in animation while actually selecting and emulating the ECP-triggered pass. The frequency of this bug depends on the polling loop interval.
- In iOS 17 the new NameDrop frame does not follow the aforementioned rules fully, as the device reacts to it with an animation on the first iteration, although the response itself is still returned as presented in examples.
Each ECP frame consists of a header, version, payload and CRC:
6A XX XX... XX XX
[Header] [Version] [Payload (n)] [CRC]
- Header byte has a constant value of (HEX) 6A;
- Version number can be either 0x01 or 0x02;
- Payload: Version-dependent;
- CRC (Calculated via ISO14443A/B algorithm, according to the modulation used).
For V1 payload consists only of a single TCI:
XX XX XX
[TCI]
- TCI is a 3 byte long identifier. More info below.
For V2 payload contains terminal configuration, terminal type, terminal subtype, and data:
XX XX XX XX...
[Config] [Type] [Subtype] [Data (n)]
- Configuration byte. Responsible for feature flags and data length. More info below;
- Type contains terminal type:
- 0x00: Special (Ignore);
- 0x01: Transit;
- 0x02: Access;
- 0x03: Identity (Handoff);
- 0x05: AirDrop.
- Subtype depends on type. In most cases it has a value of 0x00;
- Data. Its content and availability depend on terminal type and subtype. Detailed description below.
Configuration byte is a mandatory part of a V2 ECP payload. It consists of 4 flag bits and a length nibble:
| Bit | 07 | 06 | 05 | 04 | 03 02 01 00 |
|---|---|---|---|---|---|
| Function | Automatic pass presentment | Authentication not required | Unknown flag 1 | Unknown flag 2 | Length |
- Bit 07. Automatic pass presentment. The default value is
1. A matching pass does not appear on a screen if this value is set to0, which also disables express mode as a result. ECP frames with this value being0are not found IRL. - Bit 06. Authentication not required. The default value is
1. Disables express mode if the value is0, the device will bring up a matching pass for manual authentication instead. Access readers may set it when auth is required. Also set to0for Identity and AirDrop/NameDrop. - Bit 05-04. Meaning unknown. The default value is
0; - Bits 03-00. Defines the length of the data part (0-15 bytes). If the length does not match, the device will ignore this ECP frame.
Data is a part of the payload in V2, it contains TCIs and extra data:
XX XX XX... XX..
[TCIs (n)] [Extra data (n)]
- TCIs define an array of 3-byte-long identifiers. TCI arrays of arbitrary length may be conveyed depending on terminal type and subtype;
- Extra data contents depend on terminal type, subtype, and TCIs:
- For transit it contains a mask of supported EMV payment networks for fallback;
- For access/key readers it may contain an 8-byte-long unique reader group identifier, which allows differentiating between them for passes of the same type;
- For HomeKit it contains pairing information;
- For NameDrop it carries a 6-byte-long BLE MAC address;
- For AirDrop it carries a 6-byte-long zeroed-out value.
Terminal Capabilities Identifier (TCI) is an arbitrary three-byte-long value that establishes a reader's relation to a particular pass type (Home key, Car key), system feature (Ignore, GymKit, Identity, AirDrop, NameDrop), or reader installation (Access, Transit).
The following restrictions apply to the use of TCIs:
- TCIs intended for ECP1 cannot be used with ECP2;
- Access and Transit TCIs intended for ECP2 can be used with ECP1, but Transit TCIs may trigger GymKit pairing on Apple Watch if a matching pass is not installed on it;
- In ECP2, some TCIs are bound to a reader with a particular type.
TCI format is arbitrary, although several patterns related to the grouping of similar functionality can be established:
- VAS: grouped with the last byte having a value of 0x00, 0x01, 0x02, 0x03 depending on mode;
- Access (Car/Home/University/Office/Venue): The first byte is either 0x02/0x04/0x82 outright or has its high nibble set to 0x2, and the remaining bytes link to a particular property or system;
- Transit: The first byte is always 0x03, other two link to a particular transit agency (and their pass);
- CarKey: The first byte is always 0x01. The next three nibbles serve as a manufacturer identifier. The last nibble serves as the reader location index. This can be seen in wallet configuration JSON hosted at smp-device-content.apple.com.
A single pass may use one or more TCI values, with the latter option present in passes provisioned to work with multiple installations at the same time.
Note that CRC A/B, ECP Header, Configuration bytes are omitted from this table.
NA - not applicable; XX - any; ?? - unknown
| Name | Version | Type | Subtype | TCI | Data | Source | Description |
|---|---|---|---|---|---|---|---|
| VAS or payment | 01 | NA | NA | 00 00 00 | NA | Sniffing | VAS ECP configurations are sometimes regarded to as VASUP-A(B) |
| VAS and payment | 01 | NA | NA | 00 00 01 | NA | Bruteforce | |
| VAS only | 01 | NA | NA | 00 00 02 | NA | Bruteforce | |
| Payment only | 01 | NA | NA | 00 00 03 | NA | Bruteforce | As all other VAS frames, also serves as anti-generic-a |
| GymKit | 01 | NA | NA | c3 00 00 | NA | Bruteforce | The only way of triggering GymKit on an apple watch. There are other frames that trigger GymKit too, but they also trigger express transit for iPhones |
| Ignore: (ECP1) | 01 | NA | NA | cf 00 00 | NA | Sniffing | |
| Access: Seos: (ECP1) | 01 | NA | NA | 82 00 00 | NA | iOS firmware analysis | Only known access-related ECP v1 frame; iOS treats it as a special case |
| Ignore (ECP2) | 02 | 00 | 02 | NA | NA | Sniffing | Prevents Apple devices from reacting to the field emitted by another Apple device |
| Transit | 02 | 01 | 00 | XX XX XX | XX XX XX XX XX | Bruteforce based on TFL example | TCI refers to a transit agency, Data is a mask of allowed EMV payment networks for fallback |
| Transit: Ventra | 02 | 01 | 00 | 03 00 00 | ?? ?? ?? ?? ?? | Bruteforce | Chicago |
| Transit: HOP Fastpass | 02 | 01 | 00 | 03 04 00 | ?? ?? ?? ?? ?? | Bruteforce | Portland |
| Transit: WMATA | 02 | 01 | 00 | 03 00 01 | ?? ?? ?? ?? ?? | Bruteforce | Washington, DC |
| Transit: MBTA | 02 | 01 | 00 | 03 00 03 | 7f 00 00 00 00 | Sniffing | MBTA Boston |
| Transit: TFL | 02 | 01 | 00 | 03 00 02 | 79 00 00 00 00 | Sniffing: Payment Village/Proxmark community | Transit For London; Allows Amex, Visa, Mastercard, Maestro, VPay |
| Transit: LA Tap | 02 | 01 | 00 | 03 00 05 | ?? ?? ?? ?? ?? | Bruteforce | Los Angeles |
| Transit: Clipper | 02 | 01 | 00 | 03 00 07 | ?? ?? ?? ?? ?? | Bruteforce | San Francisco Bay Area |
| Transit: Skåne | 02 | 01 | 00 | 03 08 00 | 78 00 00 00 00 | Sniffing | Malmö |
| Transit: Navigo | 02 | 01 | 00 | 03 09 5a | 00 00 00 00 00 | Bruteforce/Sniffing | Île-de-France Mobilités Paris |
| Transit: Presto | 02 | 01 | 00 | 03 09 60 | ?? ?? ?? ?? ?? | Bruteforce | Metrolinx Toronto |
| Transit: KPT | 02 | 01 | 00 | 03 0a 85 | 78 00 00 00 00 | Sniffing | Kyiv Public Transport |
| Access | 02 | 02 | XX | XX XX XX | NA/XX XX XX XX XX XX XX XX | Assumption based on other data | TCI refers to a pass provider; Data is reader group identifier; For passes with reader identifier, having more than one with the same TCI breaks usual logic |
| Access: Venue | 02 | 02 | 00 | XX XX XX | NA | HID Reader configuration manual | |
| Access: Placeholder | 02 | 02 | 00 | 02 ff ff | NA | File | Used as a default value on development passes & unconfigured readers; Also encountered for hospitality passes, before they're configured for a particular booking |
| Access: Seos: (ECP2) | 02 | 02 | 02 | 82 00 00 | NA | iOS firmware analysis | iOS handles this as a special case, pretending that ECP1 frame was encountered instead |
| Access: Home: HomeKey | 02 | 02 | 04 | 02 11 00 | XX XX XX XX XX XX XX XX | Sniffing/File | Home Key based on the CopernicusHome applet for use with HomeKit-based locks |
| Access: Home: Aliro | 02 | 02 | 04 | 20 42 20 | XX XX XX XX XX XX XX XX | Sniffing | Home Key based on the CopernicusAliro applet for use with Matter-based locks |
| Access: Car: (No Reader ID) | 02 | 02 | 0[1/8] | XX XX XX | NA | Sniffing | |
| Access: Car: (Reader ID) | 02 | 02 | 0[5/a] | XX XX XX | XX XX XX XX XX XX XX XX | Sniffing/Bruteforce | |
| Access: Car: Pairing: (No Reader ID) | 02 | 02 | 09 | XX XX XX | NA | Bruteforce | |
| Access: Car: Pairing: (Reader ID) | 02 | 02 | 0b | XX XX XX | NA | Bruteforce | |
| Access: Car: BMW/MINI | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 0[1-3] | NA/XX XX XX XX XX XX XX XX | Sniffing/Configuration | |
| Access: Car: KIA | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 4[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Genesis | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 5[1-7] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Lotus | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 9[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: NIO (1) | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 a[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: NIO (2) | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 b[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Skoda | 02 | 02 | 0[1/5/8/9/a/b] | 01 00 e[1-6] | NA/XX XX XX XX XX XX XX XX | Sniffing | |
| Access: Car: Audi | 02 | 02 | 0[1/5/8/9/a/b] | 01 01 1[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Polestar (1) | 02 | 02 | 0[1/5/8/9/a/b] | 01 01 3[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Lynk & Co | 02 | 02 | 0[1/5/8/9/a/b] | 01 01 a[1-6] | NA/XX XX XX XX XX XX XX XX | Sniffing/Configuration | |
| Access: Car: Zeekr | 02 | 02 | 0[1/5/8/9/a/b] | 01 01 b[1-6] | NA/XX XX XX XX XX XX XX XX | Sniffing/Configuration | |
| Access: Car: Smart | 02 | 02 | 0[1/5/8/9/a/b] | 01 01 e[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Mercedes | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 0[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: RAM | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 2[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Denza | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 4[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Volvo (1) | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 9[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Polestar (2) | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 a[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: YW | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 b[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: FCB | 02 | 02 | 0[1/5/8/9/a/b] | 01 02 d[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Hyundai | 02 | 02 | 0[1/5/8/9/a/b] | 01 03 0[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Chery | 02 | 02 | 0[1/5/8/9/a/b] | 01 03 1[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Jetour | 02 | 02 | 0[1/5/8/9/a/b] | 01 03 3[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Xpeng | 02 | 02 | 0[1/5/8/9/a/b] | 01 03 8[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Volvo (2) | 02 | 02 | 0[1/5/8/9/a/b] | 01 04 0[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: SAIC Audi | 02 | 02 | 0[1/5/8/9/a/b] | 01 04 6[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Voyah (1) | 02 | 02 | 0[1/5/8/9/a/b] | 01 05 5[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: Voyah (2) | 02 | 02 | 0[1/5/8/9/a/b] | 01 05 6[1-6] | NA/XX XX XX XX XX XX XX XX | Configuration | |
| Access: Car: BYD | 02 | 02 | 0[1/5/8/9/a/b] | 01 07 0[1-6] | NA/XX XX XX XX XX XX XX XX | Sniffing/Configuration | |
| Identity | 02 | 03 | 00 | NA/00 | NA/00 | Sniffing | Only ECP frame found IRL that lacks a full TCI. Could this mean that TCI length is variable or it could be missing and the extra byte is data instead? |
| AirDrop | 02 | 05 | 00 | 01 00 00 | 00 00 00 00 00 00 | Sniffing | Sent only after device sees a NameDrop frame |
| NameDrop | 02 | 05 | 00 | 01 00 01 | XX XX XX XX XX XX | Sniffing | Data part contains a BLE MAC-address |
Source: Bruteforce - found by going over all config combinations; Sniffing - sniffed from a real reader; File - retrieved from pass file; Configuration - retrieved from smp-device-content configuration json
Frames found via brute force may be working but not actually used. In case you have a real sample - let us know if it is different or the same.
Examples omit the CRC, which needs to be calculated according to the modulation used;
-
VAS or payment:
6a010000006a 01 000000 [Header] [Version] [TCI] -
GymKit:
6a01c300006a 01 c30000 [Header] [Version] [TCI] -
Ignore
6a01cf00006a 01 cf0000 [Header] [Version] [TCI] -
Access: Demo:
6a02c3020002ffff6a 02 c3 02 00 02ffff [Header] [Version] [Config] [Type] [Subtype] [TCI] -
NameDrop:
6a02890500010001deadbeef69696a 02 89 05 00 010001 deadbeef6969 [Header] [Version] [Config] [Type] [Subtype] [TCI] [Data] 1000 1001 [NA] [Payload length] -
Access: Car: Pairing: Mercedes:
6a02c302090102016a 02 c3 02 09 010201 [Header] [Version] [Config] [Type] [Subtype] [TCI]Replace TCI for any car TCI given in the table to get pairing prompt for other brand
Note that for examples to work 8-bit byte setting should be set in case of NFC-A, 2-byte CRC has to be appended beforehand.
If you have a Proxmark3, ECP can be configured using hf 14a config --pla <frame> option for automatic use with other hf 14a commands.
Express mode for EMV is triggered as a fallback in case a pass for a particular transit TCI has not been found in a system. EMV brand support mask, sometimes referred to as the open-loop mask, is contained in the last 5 bytes of the transit frame data. At the moment, the first byte is widely used, and the rightmost bit of the second byte has also been mapped in the field.
The following table presents the known encoding scheme, with bit 00 being the rightmost, and a value of 1 meaning the brand is supported:
| Byte ↓ / Bit → | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
|---|---|---|---|---|---|---|---|---|
| Byte 0 | VPAY? | ELECTRON | VISA | MASTERCARD | MAESTRO | DINERS CLUB? | DISCOVER | AMEX |
| Byte 1 | ?? | ?? | ?? | ?? | ?? | ?? | ?? | JCB |
| Byte 2 | ?? | ?? | ?? | ?? | ?? | ?? | ?? | ?? |
| Byte 3 | ?? | ?? | ?? | ?? | ?? | ?? | ?? | ?? |
| Byte 4 | ?? | ?? | ?? | ?? | ?? | ?? | ?? | ?? |
To find a bit responsible for your card network, you can modify a particular bit inside the mask. Afterward, having activated a specific card brand for express mode on your device, observe if the express mode will activate when brought near to a test reader.
Known unsupported networks (not represented in the table above):
- China Union Pay (CUP).
Known supported networks with unmapped bits (present in the field but undefined in the table):
- Cartes Bancaires (CB) — confirmed to function, but the corresponding byte/bit has not yet been identified.
When EMV is used with express mode, it modifies transaction parameters that need to be used for a device to produce a successful transaction + check mark. Requirements regarding the EMV transaction seem to be dependent on a payment network brand (as each has a separate applet implementation):
- Mastercard/Maestro:
- MCC is in a transit category
- TC/ARQC + CDA
- Visa:
- Offline data authentication for online authorizations enabled
- TC/ARQC + CDA
Other card brands may have different success conditions and behavior changes. If you have any info, feel free to create a PR.
ECP2 allows enabling Terminal Requested Authentication (TRA) by clearing bit 06 of the configuration byte. When that bit is cleared, express mode stays off and the device surfaces the matching card for manual authentication.
The device enforces TRA differently depending on the credential technology, preventing a relay from an express-enabled reader to a TRA-activated one:
- Unified Access (Aliro/Home/Car/Access): readers send a transaction-type flag that also feeds the cryptographic input. The device’s policy determines whether that flag requires manual authentication;
- MIFARE DESFire: specific DESFire application IDs can be marked “not available with express mode,” so a reader cannot select them until the user authenticates manually;
- HID Seos: To be verified; Likely similar to DESFire, where individual ADFs can be flagged as unavailable in express mode.
Early ECP research found occasional references to "Enhanced Contactless Protocol", which was assumed to be a mistake or misinterpretation in the original promotional materials.
In one of the reader promotional documents, "Enhanced Contactless Protocol" arose once again, this time in the context of "DESFire ECP compatibility mode", which rang a bell.
After a bit of analysis, it turned out that DESFire protocol indeed has a special command created specifically for Apple devices, the sole purpose of which is to notify a device that a transaction has been done successfully.
This leads to the thought that ECP (Polling) and ECP (Protocol) are indeed two different terms when used by Apple and/or their partners. In conclusion, a new explanation for ECP (Protocol) has been formulated.
Enhanced Contactless Protocol is a protocol that implements commands that allow the reader to explicitly notify an end device about the state of a transaction, without resorting to making any assumptions about a particular command sequence or operations that need to be done for a transaction to be deemed successful (or not).
An NFC transaction is deemed successful when a device produces a checkmark on a screen, followed by a notification in special cases.
Failure condition is displayed with an exclamation mark and an optional error popup/device vibration.
NFC protocols can be divided into two categories, depending on how a UX success condition is determined:
- Explicit (Enhanced Contactless Protocol);
- Implicit.
Following protocols are considered "enhanced" as they implement explicit status commands.
| Protocol name | Success condition command | Failure condition | Notes |
|---|---|---|---|
| Mifare DESFire | NOTIFY_TRANSACTION_SUCCESS/ECP_LCI(0xEE) |
DESELECT/TRESET without the command | |
| Unified Access (all CarKey, HomeKey, AccessKey, Aliro variants) | OP_CONTROL_FLOW(0x3C) with success flags or DESELECT after attestation exchange |
OP_CONTROL_FLOW(0x3C) with failure flags or DESELECT/TRESET before it |
DESFire command name was made up by me as it's newly discovered, no info about it online.
Following protocols have implicit transaction status detection:
| Protocol name | Success condition | Failure condition | Notes |
|---|---|---|---|
| T-Union | DESELECT/TRESET after AID selection | DESELECT/TRESET before success condition | |
| T-Money | DESELECT/TRESET after SELECT + any successful protocol command | DESELECT/TRESET before success condition | |
| FeliCa | 0.5 second delay after TRESET if REQUEST_SERVICE(0x02) has been used. 5 second delay after TRESET otherwise |
READ/WRITE commands with invalid keys | Current success condition causes lots of confusion for users, as a top-up machine may do a TRESET and an additional POLLING for data verification, but it causes a check mark to appear thus misleading users, making them think that they can take the device out prematurely |
| EMV | Cryptogram generation (EMV mode) or magstripe data read (MAG compatibility mode) | DESELECT/TRESET before success condition | |
| VAS | Successful GET_DATA followed with a DESELECT/TRESET | DESELECT/TRESET after failed GET_DATA |
Other protocols supported by Apple Wallet, such as:
- BMAC, SPTCC (Legacy China Transit);
- Seos.
Were not researched due to lack of samples to do tests on. If you have access to any of them (on a device), feel free to add info in a PR.
Service mode, also called "Plastic card mode" is a feature that is available on some contactless passes. It is intended to be used when you need to give a device to someone to conduct service/help operations with the card.
After some experimentation, the following changes to the behavior have been noticed when this mode is activated:
- Device increases the grace period before it ends the NFC authorization;
- TRESET/DESELECT, implicit, explicit transaction end conditions do not end the NFC session;
- Device generates a transaction notification/receipt after the service mode period ends (No SE notifications to the SEP?).
Judging from this information, it is safe to assume that this mode disables regular transaction status tracking and end-condition fulfillment, allowing even an incompatible/slow/unique reader to conduct any NFC transaction sequences imaginable, with DESELECTs/TRESETs and so on. Extra time compared to regular NFC auth is also given to accommodate all possible service processes and communication delays.
Android 15 introduced three brand-new concepts to the operating system's NFC stack:
- Observe Mode - a special discovery state of the NFC controller;
- Polling Loop Annotations - non-standard frames sent by a reader in a discovery loop to provide additional context about the reader field to listening devices;
- Polling Loop Filters - Polling Loop Annotation patterns that applications request the OS to look out for.
Observe Mode works by suppressing the ability of a device to respond to external NFC polling frames by default, instead passing the information about the polling loop down to the operating system and application level, allowing the former to decide on what to do next.
Based on the polling loop characteristics, the system can take the following actions:
- Releasing the NFC lock, allowing the transaction to go through automatically (this is the way it worked before anyway).
- Requiring a user intervention for a specific action:
- Authentication. For instance, this would entirely fix an issue where an Android phone would fail EMV transaction due to not being unlocked properly, or due to an unlock happening too early.
- Credential suggestion. For instance, a non-EMV reader could be detected via the usage of REQA frames (instead of WUPA), and a user could be asked to choose an appropriate access pass if multiple cards are configured on the device.
- Credential selection. A specific polling loop annotation could indicate that a particular pass is to be used with the reader, enabling automatic use even in case of a potential AID conflict.
As a side effect, Observe Mode enables an additional feature, called "Polling Loop Filters". It allows any NFC-powered application to define a specific polling data pattern so that the operating system delivers control of the NFC communication to that application regardless of which app is set as a primary wallet application.
Those cases partially cover the feature set that ECP has, as described in Overview section, meaning that anticollision-level credential selection and NFC polling loop augmentation is now available on both Apple and Android devices.
The best way to help is to provide more samples of ECP frames and TCIs.
Especially interesting (missing) are the following:
- TCIs of transit agencies that use EMV only (transit agency list at smp-device-content):
- United Kingdom:
- London (Possibly different for buses?);
- United States:
- New York (MTA);
- Australia:
- Sydney (NSW Transport);
- France:
- Dreux;
- Lyon;
- Montargis;
- Nevers;
- Tarbes-Lourdes.
- Estonia:
- Tartu.
- Finland:
- Turku.
- Belarus:
- Minsk.
- United Kingdom:
- Access passes and keys (There might be many unknown variations, so any samples would be welcome):
- University;
- Office;
- Venues;
- Hotels;
- Multi-family housing;
- Car (Real device);
- Identity (Standalone reader). Be careful not to get on a no-fly list if you dare to go and try sniffing data from it.
Meanwhile, some other unanswered questions remain, which require further analysis and testing, such as:
- If more than one TCI can be sent by a reader;
- If TCI has a format, at least in some situations;
- If a TCI is 3 bytes long, or it could have a variable length (such as for transit), this question arose after seeing an "Identity" frame;
- If the data part is actually considered a second TCI;
- If the terminal subtype has some special encoding rules;
- What the unknown configuration byte bits are responsible for (pairing or other flags?).
If you have any findings or thoughts on this matter, feel free to discuss them in the issues section.
The easiest way of retrieving useful information that doesn't require any special tools is .pkpass file analysis.
It can be done in the following way:
- Extract pass files:
- On macOS, from
~/Library/Passes/Cards/; - On jailbroken iPhone from
~/Library/Passes/Cards/; - On a non-jailbroken iPhone, via an encrypted backup extracted at path
HomeDomain/Library/Passes/Cards(Whoever patched it, send my regards).
File extraction can be done using an app such as iMazing, iOSBackup or similar tools.
- On macOS, from
- Enter the folder with passes;
- Click on any
.pkpassfile, select "Show Package Contents" - Open the
pass.jsonfile. - Inside the file, search for keywords
tci,openloop,ecp,transit,automatic,selection,express, as in example:Other fields removed to reduce space taken{ "formatVersion":1, "passTypeIdentifier":"paymentpass.com.apple", "teamIdentifier":"Apple Inc.", "paymentApplications":[ { "secureElementIdentifier":"133713371337", "applicationIdentifier":"69696969696969696969", "applicationDescription":"Mastercard", "defaultPaymentApplication":true, "supportsOptionalAuthentication":1, "automaticSelectionCriteria":[ { "type":"ecp.2.open_loop", "supportsExpress":true, "openLoopExpressMask":"0800000000" } ], "supportedTransitNetworkIdentifiers":[] } ] }
Here we see that a Mastercard has automatic selection criteria0800000000which corresponds to00001000in binary for the first byte of transit data mask.
All cards can be analyzed the same way.
The second way of collecting information about ECP is via sniffing.
Thanks to Observe Mode feature introduced in Android 15, most flagship mobile handsets now have an ability to sniff polling frames without the use of any external hardware.
Android 15 Observe Mode Demo App can be used to collect data about the polling loop, including all custom polling frames such as ECP.
Sniffing can also be done using the functionality of a device like Proxmark (Easy or RDV2/4) connected to a Proxmark client inside of Termux running on an Android phone.
A couple of tidbits encountered:
- On first use, the app failed to connect directly to Proxmark3 because Termux did not attach the device; installing TCPUART to forward the serial connection over the local network allowed use inside the Termux Proxmark client;
- Some Android phones won't power Proxmark properly through direct connection. Connecting via a USB-C to USB-A dongle can help to overcome this issue.
More info on installing and running Proxmark client on your Android device here.
The command needed to collect traces is hf 14a sniff, after activating the command hold the Proxmark near a reader for a couple of seconds. In some cases, it is needed to tap/touch the reader in order to wake it up as it might not poll to save energy.
After that, press a button on a device, and traces will be downloaded and can be viewed with a hf 14a list command. You'll know which ones are the ones.
Some other devices might also be able to sniff the frames, but none can be recommended here without firsthand validation.
For the most dedicated people, there is a third method of finding useful information.
It is possible to find ECP frames using sheer brute force.
To do that, you have to program an NFC reader that does the following in a loop:
- Turn off the RF field;
- Wait 5-10 seconds;
- Turn on the RF field;
- Attempt to increment/modify one of the values in an ECP frame;
- Send the ECP frame 3 times in 0.5-second intervals;
- Attempt wake-up with anticollision; If successful continue, otherwise return to step 1;
- Observe results, save useful info (uid, ats, sak, etc) try doing PPSE, DESFire functions, selecting basic AIDs.
A couple of tips:
- If you plan on running the brute force while not nearby, you can record a video of your device.
- Before brute forcing, decide what is your goal, what feature or pass you might want to activate, this will define which values you'll have to try changing.
- Do not try to bruteforce all values mindlessly. From the info available we see that most combinations are done using changing terminal type, subtype, and first and last bytes of TCI.
- If you find any mistakes/typos or have extra information to add, feel free to raise an issue or create a pull request;
- Material provided here has been collected using publicly available methods, without access to the original specification. Do not consider the provided material as the only source of truth, as some assumptions and descriptions made might end up being false or not fully correct. This repository intends to get as close as possible to the truth, which might take some time and collaboration.
- Information provided in this repository is intended for educational, research, and personal use. Its use in any other way is not encouraged.
- Beware that ECP, just like any other proprietary technology, might be subject to legal protections depending on the jurisdiction. The mere fact of it being reverse-engineered does not always mean that it can be used in a commercial product as-is without causing an infringement. For use in commercial applications, you should contact Apple through official channels in order to get approval.
- For tips on implementation, code examples, an overview of verified modules, refer to Examples directory;
- For a code-friendly database of ECP frames, refer to the Resources directory.
- Resources that helped with research:
- Code analysis:
- Apple resources:
- Apple Developer Documentation;
- Apple Wallet configuration json;
- Apple mention of ECP as Enhanced Contactless Protocol;
- Apple Platform Security (Archive).
- Apple Wallet Access Program (Archive) - official description of Enhanced Contactless Polling;
- Google Patents - Scalable wireless transaction system - a patent registered by Apple, which contains mentions of
ECP,TCI,VASUP-A.
- Technical resources:
- Projects that inspired research:
- Arduino Octopus Card Reader - sparked interest in trying to replicate Express Mode, which led to ECP;
- Practical EMV: Express Transit exploit - sparked interest in ECP research. ECP info there was redacted, don't bother looking for it;
- TFL ECP frame found by Payment Village.
- Forums:
- Device operation manuals:
- Device brochures:
- Springcard - Springpass (Archive) - mentions that VAS ECP frame is called VASUP-A;
- Chip brochures (with ECP mentions):
- Chip datasheets:
- Other:
- Devices and software used for analysis:
- Proxmark3 Easy - used to sniff ECP frames out. Proxmark3 RDV2/4 can also be used;
- Proxmark3 Iceman Fork - firmware for Proxmark3;
- RFID Tools app - app that can be used to control OFW Proxmark RDV4 from an Android device while in field;
- Android 15 Observe Mode Demo App - app used to collect polling frame information with a regular mobile phone;
- Termux - can be used to run Iceman Fork Proxmark client in field;
- TCPUART transparent Bridge - used to connect Proxmark to a client running in Termux;
- iOSBackup - used to extract data from an encrypted iOS backup;
- PN532, PN5180, ST25R3916 - chips used to test homebrew ECP reader implementation.








