ESPHome has NFC reader support (ST25R, PN532) but no standardised ISO 14443-4 (ISO-DEP) interface. Each reader implements APDU handling differently or not at all. This blocks higher-level protocols like Aliro, Apple Home Key, and CCC Digital Key from being implemented as reusable components.
The Aliro protocol implementation and the NFC driver should be separate concerns with a clean interface between them.
┌─────────────────────────────────────────────────┐
│ Protocol Components (independent, swappable) │
│ │
│ ┌──────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ Aliro │ │ Home Key │ │ Future... │ │
│ │ Reader │ │ Reader │ │ │ │
│ └─────┬────┘ └─────┬──────┘ └──────┬──────┘ │
│ └──────────────┼────────────────┘ │
│ │ │
│ NfcIsoDep + NfcAidHandler + NfcPollingAnnotation
│ (common contracts) │
│ │ │
├───────────────────────┼─────────────────────────┤
│ NFC Reader Drivers │ (implement interfaces) │
│ │ │
│ ┌─────────┐ ┌──────┴───┐ ┌───────────────┐ │
│ │ ST25R │ │ PN532 │ │ PN7150 / .. │ │
│ └─────────┘ └──────────┘ └───────────────┘ │
└─────────────────────────────────────────────────┘
All interfaces live in the existing nfc component (esphome/components/nfc/). Readers opt in by implementing whichever they support.
// Reader capability matrix:
// ST25R: Nfcc + NfcIsoDep + NfcPollingAnnotation
// PN532: Nfcc + NfcIsoDep (annotations possible, needs implementation)
// PN7161: Nfcc + NfcIsoDep + NfcPollingAnnotation (via proprietary NCI tag)
// PN7160: Nfcc + NfcIsoDep (no annotation support)
// RC522: Nfcc only (Mifare, no ISO-DEP)ISO 14443-4 APDU transport. The driver handles all ISO-DEP framing internally (RATS/ATS, I-Block toggling, chaining up to 2KB, WTX). Protocol components just send and receive complete APDUs.
class NfcIsoDep {
public:
/// Check if current tag supports ISO-DEP (SAK bit 0x20)
virtual bool is_isodep_active() const = 0;
/// Send a complete APDU and receive the complete response.
/// Driver handles I-Block framing, block number toggling,
/// chaining (up to 2KB), and WTX internally.
/// Returns true if response received; resp/resp_len contain
/// response data including SW1 SW2.
virtual bool transceive_apdu(const uint8_t *cmd, size_t cmd_len,
uint8_t *resp, uint8_t &resp_len) = 0;
/// Get the UID of the currently selected tag
virtual std::string get_uid() const = 0;
/// Get the ATS (Answer To Select) from RATS exchange
virtual bool get_ats(uint8_t *ats, uint8_t &ats_len) const = 0;
};Protocol components register interest in specific AIDs. When a Type 4 tag enters the field:
- Driver activates ISO-DEP (RATS)
- Tries each registered AID via SELECT in registration order
- First handler whose SELECT gets SW 9000 owns the session
- That handler runs its protocol via
transceive_apdu() - Non-matching AIDs return SW 6A82 ("not found") — costs ~5ms each, negligible
class NfcAidHandler {
public:
/// The AID this handler is interested in
virtual const uint8_t *get_aid() const = 0;
virtual size_t get_aid_length() const = 0;
/// Called when SELECT for this AID succeeds.
/// Handler runs its full protocol using isodep->transceive_apdu().
/// select_response/len contains the FCI template from SELECT.
virtual void on_aid_selected(NfcIsoDep *isodep,
const uint8_t *select_response,
size_t response_len) = 0;
};This solves ESPHome feature request #3068 — Apple Home Key, Aliro, NDEF Type 4, and any future protocol coexist on the same reader without monopolising it.
Based on feedback from kormax.
Mobile credentials (Aliro, Home Key, transit) require the reader to identify itself during the polling loop so the phone knows which credential to activate before the tap. This is done via annotation frames (Apple calls this Enhanced Contactless Polling / ECP).
The annotation frame is sent after WUPA if no tag was detected, every polling cycle, as a full NFC-A or NFC-B frame with CRC and 8 bits in the last byte.
This is a separate interface from NfcIsoDep because annotations are a polling-loop concern, not a transport concern. A reader might support annotations without ISO-DEP (unlikely but possible), or ISO-DEP without annotations (common - e.g., PN7160).
class NfcPollingAnnotation {
public:
/// Configure an annotation frame to be sent during the polling loop.
/// The driver sends this frame after every WUPA that gets no tag response.
/// frame_data: raw annotation bytes (e.g., ECP V2 frame, without CRC — driver adds CRC)
/// modulation: NFC-A or NFC-B
virtual bool set_polling_annotation(const uint8_t *frame_data, size_t len,
NfcModulation modulation) = 0;
/// Remove the polling annotation
virtual void clear_polling_annotation() = 0;
};The mechanism differs by chip:
| Chip Type | How the driver implements it |
|---|---|
| Raw frame chips (ST25R, PN532) | Send annotation frame via FIFO + TRANSMIT after each WUPA timeout |
| NCI chips (PN7161 only) | Configure NCI polling loop with proprietary annotation tag |
Protocol components configure their annotation at setup time:
// Aliro reader configures its ECP annotation at boot
uint8_t ecp_frame[] = {
0x6A, // ECP header
0x02, // V2
0xC8, // config: express mode enabled, 8 bytes data
0x02, // type: Access
0x04, // subtype
0x20, 0x42, 0x20, // Aliro TCI
// ... 8-byte reader group identifier appended at runtime
};
driver->set_polling_annotation(ecp_frame, sizeof(ecp_frame), NFC_A);Without this, express mode / background tap doesn't work — the user would have to manually unlock their phone and select the credential before tapping.
An ESPHome component that implements NfcAidHandler for the Aliro AID:
# User-facing config
aliro_reader:
nfc_id: my_nfc_reader # any reader implementing NfcIsoDepInternally:
- Registers AID
A000000909ACCE5501(expedited phase) - Configures ECP annotation with Aliro TCI
20 42 20(if reader supportsNfcPollingAnnotation) - On SELECT success: runs AUTH0 → LOAD CERT → AUTH1 → EXCHANGE state machine
- Handles all crypto (ECDSA P-256, ECKA-DH, HKDF-SHA256, AES-256-GCM)
- Manages credentials (Kpersistent, reader keypair, certificates)
- Exposes lock entity to Home Assistant
- Future: integrates with HA's Matter credential management (roadmap #8, Epic 2)
The protocol component has zero knowledge of NFC hardware. It works with ST25R today and any future reader that implements NfcIsoDep.
| Reader | NfcIsoDep | NfcPollingAnnotation | Work needed |
|---|---|---|---|
| ST25R (PR #15082) | Yes — send_apdu() + is_isodep_active() exist |
Possible — raw frame chip, needs annotation in polling loop | Wrap existing methods, add annotation frame to WUPA cycle |
| PN532 | No | Possible — raw frame chip | Needs RATS + I-Block implementation, then annotation |
| PN7161 | Likely | Yes — proprietary NCI annotation tag | Needs NCI integration |
| PN7160 | Likely | No — hardware limitation | ISO-DEP only |
| RC522 | No — Mifare only, no ISO-DEP | No | Not applicable |
- Define
NfcIsoDep,NfcAidHandler,NfcPollingAnnotationinesphome/components/nfc/ - Implement all three on ST25R (wrapping existing capabilities)
- Add AID handler registration and SELECT dispatch loop
- Contribute upstream to ESPHome (addresses feature request #3068)
- Implement as
NfcAidHandlerfor Aliro AID - Crypto stack: ECDSA P-256, AES-256-GCM, HKDF-SHA256 (via mbedTLS / ESP-IDF)
- Credential store: reader keypair, Kpersistent entries, reader certificate
- Lock entity for Home Assistant
- Expose as Matter door lock with Aliro feature flags
- Implement AliroDelegate for credential provisioning via Apple Home / Google Home / SmartThings
- Aligns with HA roadmap issue #8 (Aliro is Epic 2)
- Apple Home Key handler (same
NfcAidHandlerinterface, AIDA0000008580101) - Other access control protocols (CCC Digital Key, etc.)
- ESPHome ST25R PR #15082 — ISO-DEP +
send_apdu()implementation (draft) - ESPHome feature request #3068 — NFC Type 4 AID handler request
- kormax/aliro — Aliro protocol research
- kormax/python-aliro-reader — Python Aliro reader PoC
- kormax/apple-enhanced-contactless-polling — ECP / annotation frame documentation
- dogboy21/aliro-rs — Rust Aliro reader library
- kormax/apple-home-key-reader — Home Key reader (Python)
- HA roadmap #8 — HA credential management + Aliro
- Matter SDK AliroDelegate — 29+ Aliro PRs merged
- Aliro 1.0 Specification — CSA, February 2026