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.
112 lines
5.6 KiB
Markdown
112 lines
5.6 KiB
Markdown
# 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.
|