Skip to content

fix: standards conformance fixes from a cross-implementation review - #19

Merged
Lakr233 merged 10 commits into
mainfrom
fix/standards-conformance
Oct 17, 2026
Merged

Lakr233 merged 10 commits into
mainfrom
fix/standards-conformance

Conversation

@Lakr233

@Lakr233 Lakr233 commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes found by reviewing every supported card family against its open standard. Closed-source readers were decompiled as cross-references: NXP TagInfo / TagWriter (with NXP's own SDK embedded), NFC TagInfo, NFC Tools, ST25 NFC Tap, Japanese IC readers, passport SDK demos, and My Number / transit apps. Each fix was made only after a test failed on the old code, with the standard clause cited in the test.

Area Fix Evidence
Passport PACE runs with the MF selected; the eMRTD application is then selected under PACE secure messaging. The plain SELECT happens only before BAC. ICAO 9303-11 §4.2 (step 4, Note on the current DF), Appendix K; Veriff and ReadID/JMRTD do the same
Passport INTERNAL AUTHENTICATE uses extended Le when the signature or its SM reply does not fit in 256 bytes (RSA above 1848 bits with 3DES SM, above 1784 bits with AES) ISO/IEC 7816-4 §5.1, ICAO 9303-11 §6.1.4 Note
ISO 15693 The block lock flag reads only bit b1 (Lock_flag) ISO/IEC 15693-3; ST25 SDK and NFC TagInfo mask & 1
ISO 15693 Tags with more than 256 blocks are sized with Extended Get System Information (3Bh), falling back to 2Bh ISO/IEC 15693-3:2019; 2Bh's one-byte block count cannot express more than 256 blocks
NDEF Type 2 readNDEF reads up to the data area declared in the CC (NTAG I2C, NTAG203); an erased tag reads nothing extra NFC Forum T2T 1.0 §6.1
NDEF MB is required on the first record and only on the first record NDEF 1.0 §3.2.1
FeliCa FeliCaFrame.POLLING is 00h (it was 04h, which is Request Response) JIS X 6319-4; four apps agree
DESFire 91 AD is decoded as authenticationDelay; authenticateEV2First refuses PCDCap2.1 bit 1 (LRP) NT4H2421Gx Table 23, §9.1
Japan IC History records carry entry / exit region metadata from byte 15; process type 17h is a top-up No public standard; Metrodroid plus the MJS, Jobcan and yanzm readers agree

Not changed because they are unproven: the Octopus default offset, how the T-Union balance is combined, the My Number header check, and ISO 15693 Protocol_Extension.

Needs hardware validation

  • Passport: the new SELECT order after PACE, and extended Le for RSA-2048 AA. The standard requires both, but neither has been tried on a real passport.
  • ISO 15693: 3Bh on an ST25DV64K. Formatting a 2048-block tag writes every block. A timeout midway still leaves a valid empty NDEF, because the CC and the 03 00 FE TLV are written first.

Tests

On the iOS Simulator the package tests pass (1068 tests, 69 suites) and the Example app builds. After the fixes, an independent adversarial review of the combined diff found no regressions; its doc and erased-tag findings are fixed in the last commit.

#18 also adds an ## [Unreleased] CHANGELOG section, so whichever PR merges second needs a trivial CHANGELOG merge.

🤖 Generated with Claude Code

Lakr233 and others added 10 commits October 17, 2026 14:13
JIS X 6319-4 assigns 00h to Polling and 04h to Request Response; the
constant held 04h and the test cited a wrong source. Add REQUEST_RESPONSE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DESFireStatus.authenticationDelay (91 AD, NT4H2421Gx Table 23).
- authenticateEV2First rejects PCDCap2.1 bit 1 before sending: it requests
  LRP secure messaging (NT4H2421Gx §9.1), whose answer carries an extra
  AuthMode byte the EV2 path would misread. The framing test used that bit
  with an AES transcript and now uses a capability value without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History records with stations carry the entry / exit region from byte 15
(bits 7-6 / 5-4) in metadata, since a station code is unique only with its
region, and process type 17h (bus auto-charge) is a top-up. Metrodroid
SuicaTrip and the MJS, Jobcan and yanzm readers agree on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed Le for long AA signatures

Follow the Chip Access Procedure of ICAO 9303-11 §4.2: run PACE right
after EF.CardAccess with the Master File as current DF, then select the
eMRTD application under PACE secure messaging. The plain SELECT now only
precedes BAC.

Ask for an extended INTERNAL AUTHENTICATE response (DO'97 0000, outer Le
0000) when the DG15 signature or its secure messaging reply exceeds 256
bytes (ISO/IEC 7816-4 §5.1, ICAO 9303-11 §6.1.4 Note), so RSA-2048 and
RSA-3072 Active Authentication keys can return their signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ISO/IEC 15693-3 GET MULTIPLE BLOCK SECURITY STATUS returns one byte per
block whose b1 is Lock_flag and whose b2-b8 are RFU. ST tags use RFU bits
for password protection, so `status != 0` reported those blocks as locked.
Map the bytes through the new pure ISO15693BlockSecurityStatus, which masks
with 0x01 like ST25 SDK and NFC TagInfo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET SYSTEM INFORMATION (2Bh) holds the block count in one byte (value - 1),
so it cannot describe more than 256 blocks, and the library only used it:
the extended-addressing paths for tags above 256 blocks were unreachable
and a 2048-block ST25DV64K was dumped, read and formatted as 256 blocks.

ISO15693Reader.getSystemInfo() now sends EXTENDED GET SYSTEM INFORMATION
(ISO/IEC 15693-3:2019, 3Bh, Info flags requesting the memory size) when 2Bh
reports 256 blocks or no memory size, parses the 2-byte block count with the
pure ISO15693ExtendedSystemInfo, and takes it only when the block size
matches and the count is not smaller. A tag that rejects 3Bh, stays silent
or answers malformed keeps the 2Bh result. CoreNFC has no 3Bh call, so the
transport sends it with sendRequest and checks the response error flag.
The dump, the Type 5 NDEF paths and getISO15693SystemInfo use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NFC Forum T2T 1.0 §6.1: the data area is CC byte 2 x 8 bytes from page 4.
readNDEF took the message from a dump bounded by the product memory map,
which is a conservative 16 pages for products GET_VERSION does not
identify (NTAG I2C 1k with CC E1 10 6D 00, NTAG203 which NAKs GET_VERSION),
so a message past page 15 was reported as ndefMessageNotFound.

When the dump read its whole map without a NAK and holds no message, read
the further pages the CC declares (4 + ceil(CC2 * 8 / 4), capped at 256)
with READ, stopping at the first page the tag refuses. Writes keep the
conservative memory map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NFC Forum NDEF 1.0 §3.2.1: MB is set on the first record of a message and
on no other. The parser only checked that MB appeared somewhere, so it
accepted `11 01 01 55 00 D1 01 01 55 00` (first record without MB) and
`91 01 01 55 00 D1 01 01 55 00` (MB twice). Check MB against the record
position, chunks included. ME handling (§3.2.2: nothing after the ME
record, ME required on the last one) was already enforced; tests now
cover it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Type 2 readNDEF reads past the memory map only when the map holds no
  complete NDEF TLV; an erased tag (03 00) no longer triggers up to 200
  extra READs, whose NAK would send the tag to IDLE before a later write.
- Docs: a protected SELECT refused after PACE throws secureMessagingError
  only for a protected status; a plain status word surfaces as
  unexpectedStatusWord.
- CHANGELOG notes the new DESFireStatus case for exhaustive switches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Lakr233
Lakr233 merged commit d8ae241 into main Oct 17, 2026
1 of 4 checks passed
@Lakr233 Lakr233 mentioned this pull request Oct 17, 2026
@Lakr233
Lakr233 deleted the fix/standards-conformance branch October 17, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant