open-standards/openpay/docs/03-emv-progress.md
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

5.6 KiB
Raw Permalink Blame History

Phase 3 — op-emv Complete

Status: Draft v0.3 Date: 2026-05-17

What shipped

A zero-allocation BER-TLV codec for EMV payment payloads. This is what the FFI layer (op-ffi-swift, op-ffi-jni) will hand bytes from ProximityReader on iOS and IsoDep on Android, and what op-fraud will walk to extract feature vectors.

Modules

Module Responsibility
error Sealed Error enum with byte-offset reporting on every variant.
tag Tag (u32-packed 1–4 byte tag), TagClass, 22 named EMV tag constants.
stream TlvIter + TlvRef<'a> — zero-alloc, no_std-compatible streaming decoder.
tree Tlv + TlvBody — heap-allocated tree with find() and encoded_len(). Behind std feature.

Design choices

  • Zero alloc on the hot path. TlvIter borrows from the input slice and never allocates. The secure-element interface and on-device fraud pre-pass use this layer.
  • no_std-compatible. Default feature is std; turn it off to ship into bare-metal contexts.
  • #![forbid(unsafe_code)]. Every index is bounds-checked. Combined with property tests, the parser is panic-safe on arbitrary bytes — critical because the secure element hands us untrusted input.
  • Padding tolerated. EMV Book 3 §B.3 permits 0x00 bytes between TLVs; the iterator silently skips them.
  • Error variants carry byte offsets. Debugging a parse failure on a 1 KB EMV response without knowing where in the bytes it failed is miserable; every error variant carries the offending offset.

Verified ground truth

Claim Source / verification
BER-TLV tag is 1–4 bytes EMV Book 3, Annex B
Multi-byte tag indicator = lower 5 bits of first byte = 0x1F EMV Book 3 §B.1
Tag continuation flag = bit 8 of subsequent bytes EMV Book 3 §B.1
Constructed bit = bit 6 of first tag byte EMV Book 3 §B.1
Short-form length = 0–127 in a single byte EMV Book 3 §B.2
Long-form length: 0x8N + N value bytes, N ≤ 4 EMV Book 3 §B.2
Indefinite length (0x80 alone) banned in EMV EMV Book 3 §B.2
FCI Template tag 6F is constructed Application class Independently verified via Python computation
DF Name 84 is primitive Context-Specific (NOT Application) Independently verified — caught and corrected during testing
FCI Proprietary A5 is constructed Context-Specific Independently verified
All 22 named tag constants' wire-length classifications Independently verified by Python script
Canonical FCI test vector parses correctly Hand-decoded the 28-byte sequence; assertions match
Tap-to-Pay flat byte offsets (0, 9, 18, 23, 28, 33) Independently verified by Python walk

Test coverage

File Tests Notes
tag.rs 16 Tag class, P/C bit, multi-byte parsing, round-trip write/read
stream.rs 16 Length forms, indefinite rejection, padding, offsets, constructed children
tree.rs 6 Owned tree, find, encoded_len, nested parsing
conformance.rs 9 Real vectors: FCI template, Tap-to-Pay flat, Tap-to-Pay nested
properties.rs 3 proptest: no panics on random bytes, stream/tree agreement
Total Phase 3 50
Cumulative (Phases 1+2+3) 112

Conformance vectors

File Contents Source
fci_template.hex 6F1A840E315041592E5359532E4444463031A5088801025F2D02656E Canonical Payment System Environment FCI from EMV Book 1 Annex A. Selects the 1PAY.SYS.DDF01 directory; declares English language; SFI = 2.
taptopay_flat.hex 6 standard transaction-data tags $1.00 USD purchase, USA, 2026-05-17. Synthetic but byte-for-byte what a terminal kernel emits.
taptopay_nested.hex Same fields wrapped in E1 private constructed Tests recursion.

Bugs caught during construction (kept for the record)

  1. DF_NAME class assertion was wrong. I initially asserted Tag::DF_NAME.class() == TagClass::Application. The Python cross-check showed 0x84 starts with bits 10, making it ContextSpecific. Fixed before any phase artifact shipped.

  2. Nested-find test vector was malformed. My first draft had 6F 0C A5 0A ... — but A5's declared length (10) exceeded its actual inner content (8 bytes). Python overrun-detection caught it. Corrected to 6F 0A A5 08 ....

Both bugs were caught because every numerical claim in the test suite is independently verified by Python before committing to Rust. The same Python scripts live in our verification log; they're how we keep ourselves honest without a Rust toolchain in the sandbox.

What is NOT yet implemented (deferred)

  • Encoder. We can decode any EMV BER-TLV blob. Encoding from a Tlv tree back to bytes is straightforward but waits on Phase 4 (op-rails-card) because that's where we need it: producing the response payload to send back through the PSP for refunds and reversals.
  • EMVCo certificate validation. Verifying the issuer-side cryptogram (9F26) requires the public-key infrastructure: schemes' root CAs, issuer certificates, signed static/dynamic data. That's a substantial subsystem with its own crate — op-emv-crypto — deferred to after op-rails-card is wired up.
  • Fuzz corpus. We have property tests; a dedicated cargo-fuzz corpus with AFL-discovered crashes goes in once the encoder lands.

Next: Phase 4 — op-rails-card

The PSP adapter trait plus the first concrete driver: Hyperswitch. This is where the verified-but-deferred ISO 20022 builder bindings get wired up and where the EMV decoder feeds real Tap-to-Pay payloads into a sandbox HTTPS flow.