# emv-3ds
**EMV 3-D Secure 2.x protocol implementation in Rust.**
A zero-dependency\* Rust crate providing the complete message layer and transaction
state machine for the EMV 3DS 2.x specification — the global standard used by Visa
(3DS 2.0/2.2), Mastercard (Identity Check), American Express (SafeKey 2.0),
and all major card networks to perform strong customer authentication (SCA) during
card-not-present payments.
[](https://crates.io/crates/emv-3ds)
[](https://docs.rs/emv-3ds)
[](LICENSE)
[](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html)
\*Runtime dependencies are `serde`, `serde_json`, `uuid`, `thiserror`, and `chrono` only.
---
## What is EMV 3-D Secure?
EMV 3DS (3-D Secure version 2.x) is the payment authentication protocol defined by
[EMVCo](https://www.emvco.com/emv-technologies/3d-secure/) that enables issuers to
verify cardholder identity during e-commerce transactions without redirecting to a
static password page.
The protocol involves three parties:
| 3DS Server | 3DSS | Merchant-side component; sends AReq, receives ARes |
| Directory Server | DS | Card-scheme routing layer (Visa, Mastercard) |
| Access Control Server | ACS | Issuer-side component; authenticates cardholder |
A frictionless or challenge transaction:
```
Frictionless: 3DSS ──AReq──► DS ──► ACS ──ARes(Y/A)──► 3DSS
Challenge: 3DSS ──AReq──► DS ──► ACS ──ARes(C)───► 3DSS
browser ──CReq──► ACS ──CRes(Y/N)──► 3DSS
Decoupled: 3DSS ──AReq──► DS ──► ACS ──ARes(D)───► 3DSS
ACS ──RReq──► 3DSS ──RRes──► ACS
```
DS preparation (card range negotiation):
```
3DSS ──PReq──► DS ──PRes(card ranges + threeDSMethodURL)──► 3DSS
```
---
## Features
- **All nine protocol messages** — `AReq`, `ARes`, `CReq`, `CRes`, `Erro`, `PReq`, `PRes`,
`RReq`, `RRes` with every field from the EMVCo 3DS Core Specification 2.3.
- **Correct wire format** — `#[serde(rename)]` for every acronym field
(`threeDSServerTransID`, `acsTransID`, `dsTransID`, `acsURL`, `threeDSMethodURL`) that
`rename_all = "camelCase"` would mangle.
- **Transaction state machine** — type-safe lifecycle from `Created` through
`AwaitingARes` → `AwaitingCRes` / `AwaitingRReq` → `Authenticated` / `NotAuthenticated` / `Failed`,
with invalid-transition errors.
- **Card range negotiation** — `PreparationResponse::range_for_pan()` to look up the
card range entry (including `threeDSMethodURL`) for a given PAN prefix.
- **Decoupled authentication** — `AwaitingRReq` state, `receive_rreq()` transition, and
`ResultsResponse::acknowledge()` builder for the ACS callback flow.
- **Coded value enums** — `TransStatus`, `Eci`, `ChallengeIndicator`,
`MessageVersion`, `TransStatusReason` (21 codes), `ActionIndicator`, `AcsAuthMethod`,
`AuthenticationType`, `ResultsStatus`, and all other spec enumerations.
- **ECI / liability shift helpers** — `Eci::has_liability_shift()`,
`TransStatus::is_authenticated()`, `AuthenticationResponse::requires_challenge()`.
- **ISO 4217 currency** — `Currency` newtype with zero-padded spec string,
`Amount` with `spec_amount` / `spec_currency` / `spec_exponent` EMVCo field getters.
- **Message envelope** — `Message` enum with `from_json` / `to_json` that peeks at
`messageType` for dispatch without duplicating the field on the wire.
- **Quality-gated** — 60 tests (unit + integration + proptest), 0 `cargo-mutants`
survivors, `clippy -D warnings` clean.
---
## Quick start
```toml
[dependencies]
emv-3ds = "0.2"
```
### Build and send an AReq
```rust
use emv_3ds::message::areq::{AuthenticationRequest, MessageType};
use emv_3ds::types::{DeviceChannel, MessageCategory, MessageVersion};
let areq = AuthenticationRequest {
message_type: MessageType::AReq,
message_version: MessageVersion::V220,
three_ds_server_trans_id: uuid::Uuid::new_v4().to_string(),
device_channel: DeviceChannel::Browser,
message_category: MessageCategory::PaymentAuthentication,
three_ds_requestor_id: "your-requestor-id".into(),
three_ds_requestor_name: "Acme Payments".into(),
three_ds_requestor_url: "https://acme.example.com".into(),
acct_number: "4111111111111111".into(),
card_expiry_date: "2812".into(),
notification_url: Some("https://acme.example.com/3ds/notify".into()),
// ...optional fields omitted from JSON via skip_serializing_if
..Default::default()
};
let json = serde_json::to_string(&areq)?;
// POST json to the Directory Server endpoint
```
### Drive the state machine
```rust
use emv_3ds::transaction::TransactionState;
use emv_3ds::types::ChallengeWindowSize;
// 1. Create transaction
let state = TransactionState::new(areq);
// 2. Send the AReq — state machine gives you back the serialized message
let (state, outbound_areq) = state.areq_sent()?;
// → POST serde_json::to_string(&outbound_areq) to the DS
// 3. Receive the ARes
let ares: emv_3ds::message::AuthenticationResponse = serde_json::from_str(&ds_response)?;
let state = state.receive_ares(ares)?;
match &state {
TransactionState::Authenticated { eci, authentication_value, .. } => {
// Frictionless success — attach ECI + CAVV to auth request
}
TransactionState::AwaitingCRes { acs_url, .. } => {
// Challenge flow — redirect browser to acs_url
let creq = state.build_creq(Some(ChallengeWindowSize::W500x600))?;
// POST serde_json::to_string(&creq) to acs_url
}
TransactionState::AwaitingRReq { .. } => {
// Decoupled flow — ACS will POST an RReq back to your server
}
TransactionState::NotAuthenticated { .. } => {
// Decline or soft-decline
}
_ => {}
}
// 4a. After browser challenge: receive CRes
let cres: emv_3ds::message::ChallengeResponse = serde_json::from_str(&acs_response)?;
let state = state.receive_cres(cres)?;
// 4b. After decoupled challenge: receive RReq, send back RRes
let rreq: emv_3ds::message::ResultsRequest = serde_json::from_str(&acs_callback)?;
let (state, rres) = state.receive_rreq(rreq)?;
// POST serde_json::to_string(&rres) back to the ACS
```
### Negotiate card ranges with PReq/PRes
```rust
use emv_3ds::message::preq::{PreparationRequest, MessageType as PReqType};
use emv_3ds::types::MessageVersion;
let preq = PreparationRequest {
message_type: PReqType::PReq,
message_version: MessageVersion::V220,
three_ds_server_trans_id: uuid::Uuid::new_v4().to_string(),
..Default::default()
};
// POST to DS, receive PRes
let pres: emv_3ds::message::PreparationResponse = serde_json::from_str(&ds_response)?;
// Look up the card range for a PAN
if let Some(range) = pres.range_for_pan("411111") {
let method_url = range.three_ds_method_url.as_deref();
// Use method_url to invoke the 3DS Method before AReq
}
```
### Parse any incoming message
```rust
use emv_3ds::message::Message;
let msg = Message::from_json(&raw_json)?;
match msg {
Message::ARes(ares) => { /* handle */ }
Message::RReq(rreq) => { /* decoupled callback */ }
Message::Erro(err) => { /* abort transaction */ }
_ => {}
}
```
---
## Message types
| `AuthenticationRequest` | `AReq` | 3DSS → DS → ACS | Initiate authentication |
| `AuthenticationResponse` | `ARes` | ACS → DS → 3DSS | Outcome or challenge redirect |
| `ChallengeRequest` | `CReq` | Browser/SDK → ACS | Submit challenge data |
| `ChallengeResponse` | `CRes` | ACS → 3DSS | Challenge outcome |
| `ErrorMessage` | `Erro` | Any → Any | Protocol error |
| `PreparationRequest` | `PReq` | 3DSS → DS | Request card range data |
| `PreparationResponse` | `PRes` | DS → 3DSS | Card ranges + threeDSMethodURL |
| `ResultsRequest` | `RReq` | ACS → 3DSS | Decoupled/app auth results |
| `ResultsResponse` | `RRes` | 3DSS → ACS | Acknowledge results receipt |
---
## Transaction state machine
```
Created
│ areq_sent()
▼
AwaitingARes
│ receive_ares()
├─ Y/A ──────────────────────► Authenticated (terminal)
├─ N/U/I/R ──────────────────► NotAuthenticated (terminal)
├─ C ────────────────────────► AwaitingCRes
│ │ receive_cres()
│ ├─ Y ──────────► Authenticated (terminal)
│ └─ N/U ────────► NotAuthenticated (terminal)
└─ D ────────────────────────► AwaitingRReq
│ receive_rreq()
├─ Y ──────────► Authenticated (terminal)
└─ N/U ────────► NotAuthenticated (terminal)
Any state + receive_error() ──► Failed (terminal)
```
---
## ECI values
| `Eci::VisaFullyAuthenticated` | `05` | Visa | Full 3DS auth | ✓ |
| `Eci::VisaAttempted` | `06` | Visa | Attempted processing | ✓ |
| `Eci::VisaNotAuthenticated` | `07` | Visa | Failed / not enrolled | ✗ |
| `Eci::MastercardFullyAuthenticated` | `02` | Mastercard | Full 3DS auth | ✓ |
| `Eci::MastercardAttempted` | `01` | Mastercard | Attempted processing | ✓ |
| `Eci::MastercardNotAuthenticated` | `00` | Mastercard | Failed / not enrolled | ✗ |
---
## Roadmap
- [x] **AReq / ARes** — Core authentication messages
- [x] **CReq / CRes** — Browser/SDK challenge messages
- [x] **Erro** — Protocol error message
- [x] **PReq / PRes** — Directory Server preparation request (card range negotiation)
- [x] **RReq / RRes** — Results request for decoupled and app-based authentication
- [ ] **JWE envelope** — EMVCo-mandated end-to-end encryption for `acctNumber` and
`sdkEncData` fields
- [ ] **DS certificate management** — JWK / PKCS#12 signing for AReq integrity
---
## Spec conformance
This crate targets the **EMVCo 3DS Core Specification v2.2.0 and v2.3.0**.
The spec is available (registration required) at
[emvco.com](https://www.emvco.com/emv-technologies/3d-secure/).
EMVCo 3DS is an open standard with no royalty requirements on implementations.
---
## License
MIT — see [LICENSE](LICENSE).