NFC (Near Field Communication) é um conjunto de padrões que permite comunicação sem fio de curtíssimo alcance (tipicamente até ~4 cm) entre um iniciador e um alvo. Em sistemas de identificação, isso se traduz em um “leitor” (ex.: ESP32 + PN532) que energiza e interroga um “alvo” (ex.: um cartão MIFARE, um crachá, ou um smartphone operando em modo de emulação de cartão).
Benefícios para identificação:
- Experiência de uso: aproxima e pronto; não exige pareamento.
- Baixo consumo e tempo de transação curto.
- Compatível com ecossistema existente: cartões ISO/IEC 14443, NDEF, etc.
Modos de operação relevantes
No contexto de identificação, três modos interessam:
- Reader/Writer (leitor/escritor)
O leitor (PN532) lê/escreve dados de uma tag (cartão físico). Padrões comuns: ISO/IEC 14443 A/B (ex.: MIFARE Classic/Ultralight/DesFire), e dados em formato NDEF. - Card Emulation (emulação de cartão)
O smartphone se comporta como uma tag.
- Android: permite HCE (Host Card Emulation), em que um app responde a comandos APDU do leitor; é o caminho para entregar dados consistentes (um ID estável, criptografado, etc.).
- iOS: a emulação “aberta” é restrita. iPhones em geral não expõem HCE genérico para apps de terceiros; o acesso é limitado a casos de uso do ecossistema Apple (ex.: Apple Pay, cartões de transporte/estudante em “Express Mode”), o que não serve para enviar um ID arbitrário ao seu PN532.
- Peer‑to‑Peer (NFCIP-1/ECMA‑340/ISO/IEC 18092)
Permite troca ativa entre dois dispositivos. Hoje é raro para identificação porque o suporte nas plataformas móveis é limitado e a UX é menos previsível.
💡 Resumo prático: para identificar um telefone com um mesmo dado toda vez, no Android você usa HCE e define o protocolo (APDUs) do lado do leitor (PN532) e do app. No iOS, por restrições da plataforma, não há um caminho equivalente para um app de terceiro emular um cartão com ID estável para leitores genéricos.
Sobre UIDs “aleatórios” e por que o seu leitor vê IDs diferentes
- As tags clássicas (cartões) possuem um UID (número de série do chip) que o leitor enxerga logo no anticollision/seleção ISO14443A.
- Smartphones em modo HCE não expõem um UID fixo de tag; ao invés disso, a pilha do sistema pode anunciar identificadores temporários/aleatórios durante o anti‑colisão (ou nem aparecer como tag se não houver um serviço HCE correspondente). Isso é intencional, por privacidade e segurança: o UID nunca foi projetado para ser um identificador pessoal confiável; ele é só um detalhe de baixo nível para a logística do enlace.
- Resultado: se você tentar “identificar o telefone pelo UID”, com Android vai ver valores variáveis e com iPhone normalmente nada.
Como identificar com segurança (conceitos que guiaremos no artigo)
Basear‑se num UID estável não é seguro. Em vez disso, use dados de aplicação e protocolos com autenticação:
- App Android (HCE) entrega um identificador lógico do usuário/dispositivo dentro de um frame APDU/NDEF, preferencialmente com assinatura (chave privada no app/secure element) ou MAC (chave simétrica provisionada).
- O leitor (ESP32+PN532) valida a autenticidade do payload (ex.: verifica assinatura ECDSA/Ed25519 ou HMAC-SHA256) e associa esse ID a uma conta/permiso no seu backend.
- Opcional: desafio–resposta (o leitor manda um nonce, o app responde assinando/MACando). Isso mitiga replay e relay simples.
- iOS: como não há HCE aberto, alternativas práticas são:
- Inverter papéis: o ESP32/PN532 emula uma tag (quando suportado) e o app iOS escreve (ou lê) um registro NDEF com o ID (com autenticação). Exige o app aberto e interação do usuário.
- Out-of-band: BLE + NFC só para “toque para abrir o app”, e a autenticação forte ocorre via BLE/HTTP com o servidor.
Padrões e camadas (para situar o que virá no tutorial)
Ao longo do tutorial, vamos citar (em alto nível):
- ISO/IEC 14443‑A/B: anti‑colisão, seleção, ATR, onde o PN532 opera como leitor.
- ISO/IEC 7816‑4 (APDU): comandos/respostas usados pelo HCE no Android.
- NDEF: contêiner de dados NFC (útil, mas para identificação recomendamos APDU + assinatura).
- ESP‑IDF + PN532 (I²C/SPI/UART): driver, inicialização, polling e troca de frames, tudo sem Arduino.
O que você vai construir neste artigo
- Entender os requisitos de segurança (por que UID não serve; como evitar clonagem/replay/relay).
- Subir um projeto ESP‑IDF que fala com o PN532 e faz polling ISO14443A.
- Processar APDUs provenientes de um app Android HCE que envia um ID assinado (constante).
- Verificar a assinatura no ESP32 (ex.: Ed25519) e tomar uma ação (abrir porta, registrar presença, etc.).
- Explorar alternativas para iOS e “inversão de papéis” (PN532 como tag e app como escritor/leitor NDEF), com limitações e UX esperada.
Requisitos de Segurança no Uso de NFC para Autenticação
Por que segurança é crítica em NFC de identificação
O NFC, por operar em distâncias muito curtas, transmite a impressão de ser “seguro por proximidade”. No entanto, ataques como relay attack (prolongamento do alcance via retransmissão), replay attack (reenvio de dados previamente capturados) e clonagem de tags são viáveis se o protocolo de aplicação não adotar medidas de proteção. Em sistemas de autenticação, a segurança deve ser tratada como prioridade, já que a violação de identidade pode levar a acesso indevido, fraude ou sabotagem.
A premissa fundamental: o UID lido pelo PN532 não é suficiente para autenticar. Esse identificador pode ser forjado ou variar (como no caso dos smartphones em HCE ou iOS restrito), não servindo como prova confiável de identidade.
Ameaças comuns a sistemas NFC
- Clonagem de UID: tags com UID fixo podem ser copiadas para cartões programáveis.
- Relay Attack: atacante retransmite comunicação NFC em tempo real para outro local.
- Replay Attack: captura de uma transação válida para reutilização posterior.
- Eavesdropping: interceptação do tráfego NFC (apesar de difícil, é possível com antenas especializadas).
- App malicioso em HCE: no Android, um aplicativo comprometido pode responder como se fosse o legítimo.
Boas práticas para autenticação segura
- Identificador lógico assinado
- O app envia um payload contendo:
- ID lógico do dispositivo/usuário.
- Timestamp ou nonce (número aleatório único por transação).
- Assinatura digital ou HMAC sobre esses dados.
- No leitor, o ESP32 valida a assinatura usando chave pública pré-configurada (assinatura assimétrica) ou chave compartilhada (HMAC).
- O app envia um payload contendo:
- Desafio–resposta
- O leitor inicia o processo enviando um nonce para o app.
- O app assina ou MACa o nonce + seu ID e retorna.
- Previne replay, pois cada execução exige nova prova criptográfica.
- Criptografia de dados de aplicação
- Além da assinatura, os dados podem ser criptografados (ex.: AES-GCM) para proteger a privacidade.
- Controle de sessão e tempo
- Estabelecer janelas curtas de validade para respostas.
- Rejeitar respostas fora do intervalo de tempo ou reutilizadas.
Diferenças práticas entre Android e iOS
- Android:
- HCE permite responder a APDUs definidos por você, enviando dados autenticados.
- Pode rodar em background ou foreground dependendo do tipo de serviço NFC e permissões.
- iOS:
- Sem HCE aberto; NFC está limitado a leitura/gravação de NDEF e integração com Apple Wallet/Pay.
- Alternativa prática: inversão de papéis — PN532 emula tag, app iOS escreve dados autenticados quando tocado.
Escolha de algoritmos e chaves
- Assinatura assimétrica (ECDSA, Ed25519):
- Vantagem: o leitor só precisa da chave pública, não há risco de vazamento de chave privada.
- Boa para autenticação offline.
- Assinatura simétrica (HMAC-SHA256):
- Mais simples e rápida, mas exige proteger a chave no app (difícil no Android, quase impossível no iOS sem Secure Enclave).
- Recomendação:
- Use Ed25519 para alta segurança, rapidez e facilidade de implementação no ESP32 com bibliotecas otimizadas.
Tutorial prático: Leitura NFC com ESP32 e PN532 no ESP-IDF
Agora que entendemos o que é NFC e os requisitos de segurança, vamos colocar em prática a comunicação entre o ESP32 e o módulo PN532, usando o ESP-IDF (não Arduino).
O objetivo inicial será detectar e ler tags NFC, mas já com a estrutura preparada para processar dados vindos de um app Android em modo HCE (Host Card Emulation).
Conexão física (I²C)
O PN532 suporta comunicação via I²C, SPI ou UART. Aqui usaremos I²C pela simplicidade de conexão e disponibilidade de GPIOs no ESP32.
Exemplo de ligação (PN532 I²C → ESP32):
| PN532 | ESP32 |
|---|---|
| SDA | GPIO21 |
| SCL | GPIO22 |
| VCC | 3V3 |
| GND | GND |
💡 Nota: o PN532 opera a 3,3 V, então não é necessário conversor de nível para uso com o ESP32.
Estrutura do projeto no ESP-IDF
A estrutura mínima será:
/main
├── main.c
├── pn532.h
├── pn532.c
CMakeLists.txt
Teremos um driver básico para o PN532 (pn532.c/pn532.h) e o arquivo main.c para inicialização e loop principal.
3.3 Inicialização do I²C no ESP-IDF
#include "driver/i2c.h"
#include "esp_log.h"
#define I2C_MASTER_SCL_IO 22
#define I2C_MASTER_SDA_IO 21
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_FREQ_HZ 100000
#define I2C_MASTER_TX_BUF_DISABLE 0
#define I2C_MASTER_RX_BUF_DISABLE 0
#define PN532_I2C_ADDRESS 0x24 >> 1 // Endereço I²C do PN532
static const char *TAG = "PN532_I2C";
esp_err_t i2c_master_init(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_io_num = I2C_MASTER_SCL_IO,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
esp_err_t err = i2c_param_config(I2C_MASTER_NUM, &conf);
if (err != ESP_OK) return err;
return i2c_driver_install(I2C_MASTER_NUM, conf.mode,
I2C_MASTER_RX_BUF_DISABLE,
I2C_MASTER_TX_BUF_DISABLE, 0);
}
Explicação:
- Configuramos o I²C como mestre.
- Usamos o endereço do PN532 ajustado (shift para 7 bits).
- Ativamos pull-ups internos, mas é fortemente recomendado usar resistores de 4,7 kΩ externos para SDA e SCL.
Inicialização do PN532
#include "pn532.h"
void app_main(void) {
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C inicializado");
pn532_init();
pn532_sam_config();
while (1) {
uint8_t uid[7];
uint8_t uid_len;
if (pn532_read_passive_target(uid, &uid_len)) {
ESP_LOGI(TAG, "Tag detectada - UID:");
for (int i = 0; i < uid_len; i++) {
printf("%02X", uid[i]);
}
printf("\n");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
Implementação básica do driver PN532
O driver conterá funções para:
- pn532_init() — Inicializar o chip.
- pn532_sam_config() — Configurar o modo de operação.
- pn532_read_passive_target() — Ler UID de uma tag ou resposta de um app HCE.
Exemplo simplificado de pn532_sam_config():
void pn532_sam_config(void) {
uint8_t command[] = { 0x14, 0x01, 0x14, 0x01 };
pn532_write_command(command, sizeof(command));
pn532_read_response(NULL, 0);
}
Próximos passos
Com essa base, já conseguimos:
- Detectar tags NFC e ler seus UIDs.
- Capturar o momento em que um app Android em HCE se apresenta como tag.
Na próxima seção, vamos:
- Implementar troca de APDUs com o app Android HCE.
- Fazer o processamento seguro de um ID assinado no ESP32.
- Integrar a validação de assinatura Ed25519.
Comunicação APDU com Android HCE (ID estável + assinatura)
Nesta seção vamos resolver o problema do “ID aleatório” usando HCE (Host Card Emulation) no Android: o celular se comporta como um “cartão” e responde a APDUs definidos por você. O leitor (ESP32+PN532) envia um SELECT AID; o app Android responde com um payload estável (ID lógico) + prova criptográfica (assinatura). O ESP32 valida a resposta e então toma a ação (abrir/registrar/etc.).
Linha do tempo do fluxo:
- PN532 detecta alvo ISO14443‑A (InListPassiveTarget).
- ESP32 envia APDU
SELECT AIDvia InDataExchange.- App Android (HCE) recebe no
processCommandApdu()e responde comID || TIMESTAMP || NONCE || ASSINATURA || 0x9000.- ESP32 verifica assinatura → se válida, identifica o aparelho.
Conceitos rápidos (APDU + HCE)
- APDU (ISO/IEC 7816‑4): pacotes do tipo comando/resposta.
- Exemplo
SELECT AID:00 A4 04 00 Lc AID 0000CLA,A4INS (SELECT),04 00P1P2 (seletor por AID),Lc(tamanho do AID),AID(bytes),00Le (máximo).
- Exemplo
- AID (Application Identifier): identificador do seu “applet emulado”. Você define um AID único do seu sistema (ex.:
F0 12 34 56 78 90).
No Android, você implementa um HostApduService que “ouve” SELECT para o(s) AID(s) declarados no Manifest e responde APDUs. No iOS isso não é exposto a apps comuns (veremos alternativas mais adiante, na seção dedicada ao iOS).
Comandos PN532 usados
No modo leitor ISO14443‑A:
- InListPassiveTarget (0x4A): faz polling e seleciona o alvo (smartphone/tag).
- InDataExchange (0x40): envia dados (APDU) ao alvo já selecionado e recebe a resposta.
Observação: abstrairemos a moldura PN532 (preamble, checksums) em funções utilitárias
pn532_write_command()/pn532_read_response()criadas na seção anterior.
Estrutura do APDU no seu protocolo
Definiremos:
- AID do serviço HCE:
F0 12 34 56 78 90(exemplo — troque pelo seu). - Resposta do app (campo de dados APDU):
[ID_LEN (1B)] [ID (ID_LEN B)] [UNIX_TS (4B)] [NONCE (16B)] [SIG (64B Ed25519)] // 64 bytesSufixo padrão de status word:0x90 0x00(sucesso). - Mensagem a ser assinada pelo app:
M = "AID" || ID || UNIX_TS || NONCE(Você pode incluir versionamento e/ou domínio do sistema para evitar confusão entre apps.)
O ESP32 mantém a chave pública Ed25519Ed25519 para validar a assinatura do app. Assim nenhum segredo fica no leitor.
Código ESP‑IDF (PN532: SELECT AID e troca de APDU)
Abaixo estão trechos completos e comentados focados no fluxo APDU. Eles assumem que você já possui
pn532_write_command(),pn532_read_response()e utilitários de I²C prontos (Seção 3). Mostramos também funções novas:pn532_inlistpassivetarget(),pn532_indataexchange()e a verificação Ed25519 (via biblioteca).
pn532.h
// pn532.h
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PN532_CMD_INLISTPASSIVETARGET 0x4A
#define PN532_CMD_INDATAEXCHANGE 0x40
// AID do serviço HCE (exemplo)
static const uint8_t HCE_AID[] = { 0xF0, 0x12, 0x34, 0x56, 0x78, 0x90 };
esp_err_t pn532_init(void);
esp_err_t pn532_sam_config(void);
// Poll ISO14443-A, retorna número de alvos (0 ou 1 normalmente)
int pn532_inlistpassivetarget(uint8_t max_targets, uint8_t br_ty, uint8_t *out_buf, size_t *out_len);
// Envia APDU ao target ativo (tg = 1 normalmente) e recebe resposta
esp_err_t pn532_indataexchange(uint8_t tg, const uint8_t *tx, size_t tx_len, uint8_t *rx, size_t *rx_len);
// Helpers de alto nível para SELECT AID e leitura da resposta
esp_err_t nfc_send_select_aid_and_recv(const uint8_t *aid, uint8_t aid_len,
uint8_t *rx, size_t *rx_len);
// Verificação Ed25519 (interface)
bool nfc_verify_ed25519(const uint8_t *msg, size_t msg_len,
const uint8_t *sig64, const uint8_t *pubkey32);
#ifdef __cplusplus
}
#endif
pn532.c (núcleo da troca com HCE)
// pn532.c
#include "pn532.h"
#include "esp_log.h"
#include <string.h>
// Assume que você já implementou isto na seção anterior:
extern esp_err_t pn532_write_command(const uint8_t *cmd, size_t len);
extern esp_err_t pn532_read_response(uint8_t *buf, size_t *len, uint32_t timeout_ms);
static const char *TAG = "PN532_APDU";
// 4.4.2.1 — InListPassiveTarget (ISO14443A)
int pn532_inlistpassivetarget(uint8_t max_targets, uint8_t br_ty, uint8_t *out_buf, size_t *out_len)
{
// Comando: 0x4A, max_targets, br_ty (=0x00 para 106 kbps type A), opcional: UID len + UID (para inlist específica)
uint8_t cmd[] = { PN532_CMD_INLISTPASSIVETARGET, max_targets, br_ty };
ESP_ERROR_CHECK(pn532_write_command(cmd, sizeof(cmd)));
uint8_t resp[64];
size_t resp_len = sizeof(resp);
esp_err_t err = pn532_read_response(resp, &resp_len, 1000);
if (err != ESP_OK) return -1;
// Resposta típica: TFI + 0x4B + NbTg + [Tg, ...parametros alvo...]
if (resp_len < 3 || resp[0] != 0xD5 || resp[1] != 0x4B) {
ESP_LOGW(TAG, "Resposta inesperada InListPassiveTarget");
return -1;
}
int nb_tg = resp[2];
if (out_buf && out_len) {
size_t copy = (resp_len - 3 < *out_len) ? (resp_len - 3) : *out_len;
memcpy(out_buf, &resp[3], copy);
*out_len = copy;
}
return nb_tg;
}
// 4.4.2.2 — InDataExchange (enviar APDU ao target 'tg')
esp_err_t pn532_indataexchange(uint8_t tg, const uint8_t *tx, size_t tx_len, uint8_t *rx, size_t *rx_len)
{
// Comando: 0x40, tg, data...
uint8_t hdr[2] = { PN532_CMD_INDATAEXCHANGE, tg };
uint8_t tmp[2 + 260]; // APDUs costumam < 255B
memcpy(tmp, hdr, 2);
memcpy(tmp + 2, tx, tx_len);
ESP_ERROR_CHECK(pn532_write_command(tmp, 2 + tx_len));
uint8_t resp[300];
size_t resp_len = sizeof(resp);
esp_err_t err = pn532_read_response(resp, &resp_len, 1000);
if (err != ESP_OK) return err;
// Resposta: TFI + 0x41 + Status + Data...
if (resp_len < 3 || resp[0] != 0xD5 || resp[1] != 0x41 || resp[2] != 0x00) {
ESP_LOGW(TAG, "InDataExchange status != 0, len=%u", (unsigned)resp_len);
return ESP_FAIL;
}
size_t payload = resp_len - 3;
if (rx && rx_len) {
size_t copy = (payload < *rx_len) ? payload : *rx_len;
memcpy(rx, &resp[3], copy);
*rx_len = copy;
}
return ESP_OK;
}
// 4.4.2.3 — Helper: monta SELECT AID e recebe resposta
esp_err_t nfc_send_select_aid_and_recv(const uint8_t *aid, uint8_t aid_len,
uint8_t *rx, size_t *rx_len)
{
// APDU SELECT AID: 00 A4 04 00 Lc AID 00
uint8_t apdu[5 + 16 + 1]; // até 16B de AID, ajuste se necessário
size_t idx = 0;
apdu[idx++] = 0x00; // CLA
apdu[idx++] = 0xA4; // INS SELECT
apdu[idx++] = 0x04; // P1: seletor por AID
apdu[idx++] = 0x00; // P2
apdu[idx++] = aid_len;
memcpy(&apdu[idx], aid, aid_len); idx += aid_len;
apdu[idx++] = 0x00; // Le (máximo)
// No PN532, o target normalmente é 1 (primeiro da lista)
return pn532_indataexchange(0x01, apdu, idx, rx, rx_len);
}
Exemplo de uso no app_main.c
// main.c (trecho principal do loop)
#include "pn532.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "APP_MAIN";
// Chave pública Ed25519 do seu app (32 bytes) — exemplo fictício
static const uint8_t PUBKEY_ED25519[32] = {
0x11,0x22,0x33,0x44, /* ... preencha com sua chave real ... */ 0xAA
};
void app_main(void)
{
// init I2C + PN532 (da Seção 3)
// pn532_init();
// pn532_sam_config();
while (1) {
// 1) Poll por alvo: 1 target, 106 kbps type A
uint8_t buf[64]; size_t blen = sizeof(buf);
int nb = pn532_inlistpassivetarget(1, 0x00, buf, &blen);
if (nb <= 0) {
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
ESP_LOGI(TAG, "Alvo detectado (%d). Tentando SELECT AID...", nb);
// 2) Envia SELECT AID e recebe resposta APDU
uint8_t rx[256]; size_t rx_len = sizeof(rx);
if (nfc_send_select_aid_and_recv(HCE_AID, sizeof(HCE_AID), rx, &rx_len) != ESP_OK) {
ESP_LOGW(TAG, "SELECT AID falhou");
continue;
}
// 3) Analisa a resposta: ... dados ... 0x90 0x00
if (rx_len < 2 || rx[rx_len-2] != 0x90 || rx[rx_len-1] != 0x00) {
ESP_LOGW(TAG, "Status word != 0x9000");
continue;
}
size_t data_len = rx_len - 2;
// Layout: ID_LEN(1) | ID | TS(4B) | NONCE(16B) | SIG(64B)
if (data_len < 1 + 4 + 16 + 64) {
ESP_LOGW(TAG, "Payload curto (%u)", (unsigned)data_len);
continue;
}
uint8_t id_len = rx[0];
if (data_len < 1 + id_len + 4 + 16 + 64) {
ESP_LOGW(TAG, "ID_LEN inconsistente");
continue;
}
const uint8_t *id = &rx[1];
const uint8_t *ts = &rx[1 + id_len]; // 4 bytes (UNIX epoch)
const uint8_t *nonce = &rx[1 + id_len + 4]; // 16 bytes
const uint8_t *sig = &rx[1 + id_len + 4 + 16];// 64 bytes
// Mensagem M = "AID" || ID || TS || NONCE
uint8_t M[3 + sizeof(HCE_AID) + 64]; // tam suficiente
size_t mlen = 0;
// "AID" tag para contexto (opcional)
M[mlen++] = 'A'; M[mlen++] = 'I'; M[mlen++] = 'D';
memcpy(&M[mlen], HCE_AID, sizeof(HCE_AID)); mlen += sizeof(HCE_AID);
memcpy(&M[mlen], id, id_len); mlen += id_len;
memcpy(&M[mlen], ts, 4); mlen += 4;
memcpy(&M[mlen], nonce, 16); mlen += 16;
bool ok = nfc_verify_ed25519(M, mlen, sig, PUBKEY_ED25519);
if (!ok) {
ESP_LOGW(TAG, "Assinatura inválida — rejeitado");
continue;
}
// TODO (opcional): validar janela de tempo (TS) e nonce (anti-replay).
ESP_LOGI(TAG, "ID autenticado: %.*s", id_len, (const char*)id);
// → Execute a ação: abrir porta, logar presença, etc.
}
}
Verificação Ed25519 no ESP32
Você pode usar uma implementação portátil (ex.: TweetNaCl, ed25519-donna) como componente do ESP‑IDF. A interface usada acima é:
bool nfc_verify_ed25519(const uint8_t *msg, size_t msg_len,
const uint8_t *sig64, const uint8_t *pubkey32)
{
// Chame a função da sua lib Ed25519:
// return ed25519_verify(sig64, msg, msg_len, pubkey32) == 1;
// Abaixo deixamos como stub:
extern int ed25519_verify(const unsigned char *signature,
const unsigned char *message, size_t message_len,
const unsigned char *public_key);
int r = ed25519_verify(sig64, msg, msg_len, pubkey32);
return (r == 1);
}
Boas práticas
- Armazene a chave pública no firmware (ou obtenha de backend com TLS, se online).
- Registre ts/nonce no lado do leitor para bloquear replays por um intervalo (cache LRU simples).
- Considere usar Ed25519 por ser rápido e leve no ESP32.
App Android (HCE) — visão de alto nível
A seguir, um esqueleto didático (Kotlin) para o lado Android. Ele não compila “como‑está” sem seu Gradle/Manifest, mas mostra onde você coloca o AID e como monta a resposta.
Manifest + XML de AID
AndroidManifest.xml:
<service
android:name=".MyHceService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
res/xml/apduservice.xml:
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:requireDeviceUnlock="false">
<aid-group android:description="@string/app_name" android:category="other">
<aid-filter android:name="F01234567890" />
</aid-group>
</host-apdu-service>
Observação: o AID aqui (
F01234567890) deve casar com o usado no SELECT AID do ESP32.
Implementação do HostApduService
class MyHceService : HostApduService() {
// ID lógico estável (pode ser UUID fixo, user handle, etc.)
private val stableId = "user:alice@empresa.com".toByteArray(Charsets.UTF_8)
// Chaves Ed25519 gerenciadas pelo app (ideal: armazenar em Keystore + StrongBox quando disponível)
private val privKey: ByteArray = /* carregar do Keystore/secure storage */
private val pubKey: ByteArray = /* mesma par do leitor */
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
// Parse do SELECT AID: 00 A4 04 00 Lc AID 00
if (isSelectForOurAid(commandApdu)) {
val ts = (System.currentTimeMillis() / 1000L).toInt()
val nonce = SecureRandom().generateSeed(16)
// Mensagem M = "AID" || AID || ID || TS || NONCE
val aid = hexToBytes("F01234567890")
val M = ByteArrayOutputStream().apply {
write(byteArrayOf('A'.code.toByte(),'I'.code.toByte(),'D'.code.toByte()))
write(aid)
write(stableId)
write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(ts).array())
write(nonce)
}.toByteArray()
val sig = ed25519Sign(privKey, M) // 64 bytes
val payload = ByteArrayOutputStream().apply {
write(stableId.size)
write(stableId)
write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(ts).array())
write(nonce)
write(sig)
}.toByteArray()
return appendSw9000(payload)
}
// Caso deseje implementar outros INS (ex.: desafio–resposta separado)
return sw6A82() // File not found / AID errado
}
override fun onDeactivated(reason: Int) { /* opcional */ }
private fun isSelectForOurAid(apdu: ByteArray): Boolean {
if (apdu.size < 6) return false
val isSelect = apdu[0] == 0x00.toByte() && apdu[1] == 0xA4.toByte()
&& apdu[2] == 0x04.toByte()
if (!isSelect) return false
val lc = apdu[4].toInt() and 0xFF
if (apdu.size < 5 + lc) return false
val aid = apdu.copyOfRange(5, 5 + lc)
return aid.contentEquals(hexToBytes("F01234567890"))
}
private fun appendSw9000(data: ByteArray): ByteArray =
data + byteArrayOf(0x90.toByte(), 0x00.toByte())
private fun sw6A82(): ByteArray = byteArrayOf(0x6A.toByte(), 0x82.toByte())
private fun hexToBytes(s: String): ByteArray =
s.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
Dica de segurança no Android: guarde a chave privada no Android Keystore com StrongBox quando disponível, e nunca serialize a private key em texto claro. Como o leitor só precisa da chave pública, um vazamento do lado do leitor não compromete a identidade.
Testes e diagnóstico
- Se o SELECT AID retorna
6A 82, o AID não casou ou o serviço não está ativo. - Se retorna
90 00sem payload, confira a montagem doByteArrayOutputStreamno app. - Use logs no ESP32 para imprimir
id,tse hexdump dononceantes de verificar a assinatura. - Se a verificação falhar, valide mesma mensagem M nos dois lados e a endianness do timestamp.
Requisitos de segurança para usar NFC em autenticação
Nesta seção vamos “assentar o terreno” de segurança antes de escrever uma única linha de código. A ideia é: se o protocolo estiver certo, o hardware (ESP32 + PN532) e o app só precisam cumprir o combinado.
A matriz de ameaças (o que precisamos evitar)
- Clonagem/replay: alguém captura um frame NFC e o reenvia depois para se passar pelo seu telefone.
- IDs aleatórios/instáveis: telefones modernos não fornecem um identificador fixo de NFC. UIDs de ISO14443A podem ser randomizados (privacidade).
- Ataques de relay: um atacante “estica” a comunicação (telefone longe do leitor) e encaminha APDUs.
- Falsificação de app: um app malicioso tenta responder como se fosse o seu app.
- Exfiltração de chaves: segredos no app/servidor vazam e comprometem todo o sistema.
O que não funciona (e por quê)
- Usar UID da tag (Anti-collision ID): não é confiável com smartphones; Android pode randomizar e iOS normalmente nem apresenta UID em modo que você controle.
- Gravar um NDEF fixo com a “minha matrícula/ID”: trivial de copiar/replay. Serve para casos de identificação não segura (ex.: abrir um link), não para autenticação.
O que funciona: desafios e provas criptográficas
O desenho robusto é um protocolo de desafio–resposta sobre ISO-DEP (ISO/IEC 14443-4) usando APDUs, com chaves do lado do telefone e verificação no leitor/servidor.
Pilares
- Identidade = chave, não = ID estático
Cada telefone tem um par de chaves (ex.: Ed25519/ECDSA) gerado e fixado no dispositivo (Android Keystore/StrongBox). O “ID” do usuário vira a chave pública registrada no seu backend. - Autenticação por desafio–resposta
- O leitor (PN532) gera um nonce aleatório (ex.: 16–32 bytes) e manda via APDU.
- O app assina esse nonce (e metadados) com a chave privada e devolve a assinatura.
- O leitor/servidor valida com a chave pública daquela credencial.
Resultado: sem replay (nonce único) e sem clonagem (precisa da chave privada).
- Vincular a sessão ao leitor
Inclua no que é assinado:nonce || reader_id || timestamp. Assim, mesmo que alguém capture a resposta, ela só vale para aquele leitor e por poucos segundos. - Tempo curto de validade
Rejeite respostas comtimestampfora de janela (ex.: ±5 s). Dificulta relay. - Rotação/recuperação
Tenha mecanismo de reemitir credencial (novo par de chaves) em caso de perda/troca do aparelho.
Formato sugerido de payload (assinado)
struct AuthPayload {
uint8_t version; // ex.: 1
uint8_t reader_id[6]; // ID do leitor
uint8_t nonce[16]; // gerado pelo leitor
uint64_t unix_ms; // relógio do telefone
}
Resposta do telefone:
SIGNATURE = Sign( AuthPayload || AID || APP_VERSION )
- Algoritmo: Ed25519 (rápido e pequeno) ou ECDSA-P256.
- Transporte: APDUs sobre ISO-DEP (PN532 dá suporte).
Observação: Você também pode transportar um JWT curto (JWS compact) assinado pelo app, contendo o
noncecomo claim, se preferir algo “HTTP-like”. Em NFC não há TLS; a prova de posse é a assinatura.
Android vs iOS — o que é possível
- Android (HCE — Host Card Emulation)
- Permite emular um cartão e responder a APDUs para um AID específico do seu app.
- O app recebe
SELECT AID, depois seusGET CHALLENGE,AUTHetc. - A chave privada fica no Android Keystore (idealmente StrongBox).
- Você consegue entregar dados consistentes (a assinatura muda, a chave pública é a identidade).
- iOS (Core NFC)
- iOS não permite HCE genérico para apps de terceiros.
- O iPhone não “se anuncia” como tag customizável pelo seu app.
- Caminhos típicos no iOS:
- Escrita em tag (o inverso de papéis; ver §2.7): o telefone escreve NDEF em uma tag (real ou emulada pelo PN532); útil para provisioning, não para prova forte de identidade.
- Wallet/Passes/Keys (parcerias/entitlements específicos da Apple) — fora do alcance de apps comuns.
Conclusão prática: Autenticação ativa por NFC com telefone como “cartão” é Android via HCE. Para iOS, use fluxos alternativos (BLE + NFC de bootstrapping, QR + NFC, ou o inverso de papéis).
Esboço do protocolo (alto nível)
- Leitor → Telefone:
SELECT AIDdo seu app (APDU). - Leitor → Telefone:
GET_CHALLENGE(envianonce,reader_id). - Telefone: monta
AuthPayload, assina com chave privada (Keystore). - Telefone → Leitor:
AUTH_RESPONSEcontendoAuthPayload+signature+pubkey_id(ou handle). - Leitor: valida localmente (se tiver cache da
pubkey) ou envia ao servidor para verificação/decisão. - (Opcional): estabelecer canal de sessão curta (ex.: chaves efêmeras para trocar dados adicionais por mais 1–2 APDUs).
2.6. Boas práticas de implementação
- Geração de nonce: use TRNG do ESP32 (
esp_random()com entropy pool) e evite nonces previsíveis. - Janela de tempo: rejeite
unix_msfora de ±5 s; considere drifts. - Lista de revogação: mantenha IDs de chaves revogadas no leitor (cache) e/ou verifique online.
- Fail-closed: qualquer erro de verificação ⇒ negação.
- Proteção de chave no Android:
KeyGenParameterSpecexigindo User Authentication opcional (biometria) para assinar — se a UX permitir. - Logs auditáveis: no leitor e no servidor (hash dos eventos).
Alternativa útil: inversão de papéis (PN532 emulando tag)
Quando você precisa que iOS também participe, considere inverter:
- O PN532 emula uma tag (ex.: Type 4/ISO-DEP ou NDEF simples).
- O telefone lê/escreve: o seu app pode escrever um token assinado na tag emulada (curto prazo).
- O ESP32 valida a assinatura do token do app (sem HCE no iOS).
- Limitações: requer o app aberto/ativo; latência de interação do usuário; não é tão à prova de replay sem nonce do leitor (dá para mitigar pedindo que o app busque o nonce online e grave o token com validade de segundos).
Limitações e soluções para o problema de IDs aleatórios em smartphones
O comportamento de enviar IDs NFC aleatórios em smartphones modernos é uma decisão de privacidade tomada pelos fabricantes e especificada nas normas mais recentes do NFC Forum. Isso afeta diretamente sistemas que dependiam de UIDs fixos para identificar dispositivos.
Por que os UIDs são aleatórios
- Privacidade do usuário:
Sem randomização, qualquer leitor poderia rastrear um aparelho no mundo físico sem consentimento, usando apenas o UID da interface NFC. - Especificações técnicas:
Em modo ISO14443A (MIFARE, DESFire), o UID é negociado durante o anti-collision. O Android, desde a API 21, e fabricantes de NFC chips adotaram UIDs temporários para conexões P2P/reader. - Apple (iOS):
A restrição é ainda maior: iOS normalmente não expõe UID algum para aplicativos, mesmo quando lendo tags.
Impacto para autenticação
- Perda de identificador persistente:
Você não consegue diferenciar, de forma confiável, o mesmo telefone em leituras distintas apenas pelo UID. - Impossibilidade de “whitelist” por UID:
Não adianta gravar o UID detectado hoje para usar amanhã: ele mudará.
Soluções no Android
Host Card Emulation (HCE)
- O telefone emula um cartão ISO-DEP com AID definido pelo app.
- O app responde APDUs com um payload estável (ID lógico) + prova criptográfica.
- O ID lógico é totalmente controlado pelo desenvolvedor, garantindo consistência.
- Exemplo: NFCAndroid HCE Sample.
Aplicativo dedicado em modo leitura/gravação de tag
- Em vez de emular, o telefone grava um valor fixo (ou token assinado) em uma tag física ou emulada pelo PN532.
- Útil para integração cruzada com iOS.
Situação no iOS
- Sem HCE para desenvolvedores comuns.
- O iOS não se anuncia como tag NFC personalizada.
- Soluções viáveis:
- Emulação de tag pelo PN532 e leitura/escrita via app iOS (Core NFC).
- BLE como canal principal com NFC apenas para pairing.
- Uso de Apple Wallet passes (limitado a casos com autorização da Apple).
Estratégias para um sistema híbrido Android/iOS
Para ter um sistema que funcione nos dois ecossistemas:
- Android:
- HCE + assinatura digital
- UID controlado pelo app
- iOS:
- App lê tag emulada pelo PN532
- Tag fornece nonce
- App assina e envia a assinatura para o leitor (por BLE/Wi-Fi)
Referências úteis
- Android HCE: Documentação oficial
- NFC Forum: Especificações técnicas
- Core NFC (iOS): Documentação Apple
- PN532 datasheet: NXP PN532 User Manual
Conclusão
O uso de NFC em sistemas de identificação baseados em ESP32 + PN532 abre possibilidades interessantes para controle de acesso, autenticação e automação, mas exige atenção especial à segurança e às limitações das plataformas móveis.
A dependência de UIDs fixos já não é uma prática segura nem confiável, especialmente porque:
- No Android, o UID de tag NFC é frequentemente randomizado.
- No iOS, o UID normalmente nem é exposto, e não há HCE aberto para desenvolvedores comuns.
A solução para Android é clara: implementar Host Card Emulation para enviar um ID lógico consistente, autenticado com assinatura digital. Já para iOS, é preciso recorrer a estratégias alternativas como a inversão de papéis (leitor atuando como tag), uso de BLE/Wi-Fi como canal principal, ou NFC como inicializador de sessão.
Além disso, o uso de desafio–resposta com nonces aleatórios, timestamps curtos e identificação de leitor é fundamental para evitar ataques de replay e clonagem.
Com as práticas descritas neste artigo, é possível construir um sistema híbrido, seguro e funcional, cobrindo tanto o ecossistema Android quanto iOS, cada um com a abordagem mais adequada às suas restrições.
