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

112 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.