Skip to content

feat(emv): add nfc_emv_tvr_decode — EMV Terminal Verification Results (tag 95) field decode#71

Merged
xunholy merged 1 commit into
mainfrom
feat/emv-tvr-decode
Jun 20, 2026
Merged

feat(emv): add nfc_emv_tvr_decode — EMV Terminal Verification Results (tag 95) field decode#71
xunholy merged 1 commit into
mainfrom
feat/emv-tvr-decode

Conversation

@xunholy

@xunholy xunholy commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What

Adds nfc_emv_tvr_decode, extending the nfc_emv_* decode family (Track-2 / DOL / AFL / CVM-List / AIP) to the Terminal Verification Results (tag 95) — the terminal-side companion to last release's card-side AIP.

The TVR is the terminal's pass/fail verdict on a transaction: a 5-byte bitfield where each set bit is an exception the terminal flagged. It's exactly what you reconstruct when examining a declined or fraud transaction — which offline-auth, application-usage, cardholder-verification, risk-management and issuer-script checks failed or were skipped.

Decode (EMV 4.3 Book 3, Annex C5)

Grouped by EMV's own functional byte layout:

byte group examples
1 Offline data authentication SDA/DDA/CDA failed, ODA not performed, ICC data missing, exception file
2 Application usage version mismatch, expired / not-yet-effective, service not allowed, new card
3 Cardholder verification CVM not successful, PIN Try Limit exceeded, PIN pad missing, online PIN
4 Terminal risk management floor limit, lower/upper offline limits, random/merchant-forced online
5 Issuer script processing default TDOL, issuer auth failed, script failed before/after GENERATE AC

A flat indications list gives a one-glance read of everything flagged; clean reports an all-zero TVR. Unlike the AIP, all 40 bits are terminal-defined and stable — no contactless-kernel reinterpretation.

No confidently-wrong output

Any set RFU bit is surfaced via a per-byte note, never named. Gated structurally to exactly 5 bytes. Accepts the 5-byte value directly or a full EMV BER-TLV blob (tag 95 auto-extracted). Offline transform, transmits nothing → Low risk.

Verification

Direct mask of the published EMV bit table, so the per-bit vectors are authoritative. Tests cover: one bit per functional byte in isolation; a realistic offline-decline TVR (4000808000 = SDA failed + CVM not successful + over floor limit); the all-zero clean case; the RFU-note path; and the 5-byte structural gate.

  • gofmt / go vet clean
  • golangci-lint run ./...0 issues
  • go test -race ./internal/emv/ ./internal/risk/ ./internal/tools/ → all ok
  • registry-size (677→678), risk-consistency, gated-set tests green

Wired

internal/emv/{tvr.go,tvr_test.go}, internal/tools/emv_tvr.go, risk.go (Low), registry_size_test.go (count + comment), gap-analysis EMV row.

… (tag 95) field decode

Continues the nfc_emv_* decode family (Track-2, DOL, AFL, CVM List, AIP) to
the Terminal Verification Results (tag 95) — the terminal-side companion to
last release's card-side AIP. The TVR is the terminal's pass/fail verdict on
a transaction: a 5-byte bitfield where each set bit is an exception the
terminal flagged. It is exactly what you reconstruct when examining a declined
or fraud transaction — which offline-authentication, application-usage,
cardholder-verification, risk-management and issuer-script checks failed or
were skipped.

nfc_emv_tvr_decode cracks it. All five bytes are decoded per EMV 4.3 Book 3,
Annex C5 and grouped by EMV's own functional byte layout:

  - byte 1 offline data authentication: SDA/DDA/CDA failed, ODA not performed,
    ICC data missing, card on exception file, SDA selected
  - byte 2 application usage: version mismatch, expired / not-yet-effective,
    service not allowed, new card
  - byte 3 cardholder verification: CVM not successful / unrecognised, PIN Try
    Limit exceeded, PIN pad missing, PIN not entered, online PIN
  - byte 4 terminal risk management: floor limit, lower/upper consecutive
    offline limits, random/merchant-forced online
  - byte 5 issuer-to-card script processing: default TDOL used, issuer
    authentication failed, script failed before/after final GENERATE AC

A flat 'indications' list gives a one-glance read of everything flagged, and
'clean' reports an all-zero TVR (no exceptions). Unlike the AIP, all 40 TVR
bits are terminal-defined and stable across payment systems — no
contactless-kernel reinterpretation.

No confidently-wrong output: any RFU bit that is set is surfaced via a
per-byte note rather than named. Gated structurally to exactly 5 bytes.
Accepts either the 5-byte TVR value directly or a full EMV BER-TLV blob (tag
95 extracted automatically). Offline transform — reads bytes, transmits
nothing — so it is Low risk like the rest of the EMV decode family.

Verification: the decode is a direct mask of the published EMV bit table, so
the single-bit vectors (one bit per functional byte, in isolation) are
authoritative; plus a realistic offline-decline TVR (4000808000 = SDA failed +
CVM not successful + over floor limit), the all-zero clean case, the
RFU-note path, and the 5-byte structural gate. gofmt / go vet /
golangci-lint (0 issues) / go test -race (emv, risk, tools) all green.

Wired: internal/emv/{tvr.go,tvr_test.go}, internal/tools/emv_tvr.go,
risk.go classification (Low, consistency test green), registry_size_test.go
677 -> 678 (count + comment), and the gap-analysis EMV row.
@xunholy xunholy merged commit c36dc90 into main Jun 20, 2026
10 of 11 checks passed
@xunholy xunholy deleted the feat/emv-tvr-decode branch June 20, 2026 03:28
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