Skip to content

Instantly share code, notes, and snippets.

@JohnMcLear
Last active March 24, 2026 10:40
Show Gist options
  • Select an option

  • Save JohnMcLear/ae3910389a4d7472a147b0e34e5d12f0 to your computer and use it in GitHub Desktop.

Select an option

Save JohnMcLear/ae3910389a4d7472a147b0e34e5d12f0 to your computer and use it in GitHub Desktop.

Proposal: Modular NFC ISO-DEP + Aliro Architecture for ESPHome

Problem

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.

Proposed Architecture

┌─────────────────────────────────────────────────┐
│  Protocol Components (independent, swappable)    │
│                                                  │
│  ┌──────────┐  ┌────────────┐  ┌─────────────┐ │
│  │  Aliro   │  │  Home Key  │  │  Future...  │ │
│  │  Reader   │  │  Reader    │  │             │ │
│  └─────┬────┘  └─────┬──────┘  └──────┬──────┘ │
│        └──────────────┼────────────────┘        │
│                       │                         │
│    NfcIsoDep + NfcAidHandler + NfcPollingAnnotation
│              (common contracts)                 │
│                       │                         │
├───────────────────────┼─────────────────────────┤
│  NFC Reader Drivers   │  (implement interfaces) │
│                       │                         │
│  ┌─────────┐  ┌──────┴───┐  ┌───────────────┐ │
│  │ ST25R   │  │  PN532   │  │  PN7150 / ..  │ │
│  └─────────┘  └──────────┘  └───────────────┘ │
└─────────────────────────────────────────────────┘

Three Interfaces

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)

Interface 1: NfcIsoDep

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;
};

Interface 2: NfcAidHandler

Protocol components register interest in specific AIDs. When a Type 4 tag enters the field:

  1. Driver activates ISO-DEP (RATS)
  2. Tries each registered AID via SELECT in registration order
  3. First handler whose SELECT gets SW 9000 owns the session
  4. That handler runs its protocol via transceive_apdu()
  5. 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.

Interface 3: NfcPollingAnnotation

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.

Aliro Protocol Component

An ESPHome component that implements NfcAidHandler for the Aliro AID:

# User-facing config
aliro_reader:
  nfc_id: my_nfc_reader  # any reader implementing NfcIsoDep

Internally:

  • Registers AID A000000909ACCE5501 (expedited phase)
  • Configures ECP annotation with Aliro TCI 20 42 20 (if reader supports NfcPollingAnnotation)
  • 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.

Existing Reader Mapping

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

Implementation Plan

Phase 1: Interfaces + ST25R implementation

  • Define NfcIsoDep, NfcAidHandler, NfcPollingAnnotation in esphome/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)

Phase 2: Aliro protocol component

  • Implement as NfcAidHandler for 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

Phase 3: Matter Door Lock + Aliro

  • 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)

Phase 4: Additional protocols

  • Apple Home Key handler (same NfcAidHandler interface, AID A0000008580101)
  • Other access control protocols (CCC Digital Key, etc.)

Prior Art & References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment