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.
*Runtime dependencies are serde, serde_json, uuid, thiserror, and chrono only.
EMV 3DS (3-D Secure version 2.x) is the payment authentication protocol defined by EMVCo that enables issuers to verify cardholder identity during e-commerce transactions without redirecting to a static password page.
The protocol involves three parties:
| Role | Abbreviation | Responsibility |
|---|---|---|
| 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
- All nine protocol messages —
AReq,ARes,CReq,CRes,Erro,PReq,PRes,RReq,RReswith every field from the EMVCo 3DS Core Specification 2.3. - Correct wire format —
#[serde(rename)]for every acronym field (threeDSServerTransID,acsTransID,dsTransID,acsURL,threeDSMethodURL) thatrename_all = "camelCase"would mangle. - Transaction state machine — type-safe lifecycle from
CreatedthroughAwaitingARes→AwaitingCRes/AwaitingRReq→Authenticated/NotAuthenticated/Failed, with invalid-transition errors. - Card range negotiation —
PreparationResponse::range_for_pan()to look up the card range entry (includingthreeDSMethodURL) for a given PAN prefix. - Decoupled authentication —
AwaitingRReqstate,receive_rreq()transition, andResultsResponse::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 —
Currencynewtype with zero-padded spec string,Amountwithspec_amount/spec_currency/spec_exponentEMVCo field getters. - Message envelope —
Messageenum withfrom_json/to_jsonthat peeks atmessageTypefor dispatch without duplicating the field on the wire. - Quality-gated — 60 tests (unit + integration + proptest), 0
cargo-mutantssurvivors,clippy -D warningsclean.
[dependencies]
emv-3ds = "0.2"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 endpointuse 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 ACSuse 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
}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 */ }
_ => {}
}| Struct | Wire name | Direction | Purpose |
|---|---|---|---|
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 |
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)
| Constant | Wire | Network | Meaning | Liability shift |
|---|---|---|---|---|
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 | ✗ |
- AReq / ARes — Core authentication messages
- CReq / CRes — Browser/SDK challenge messages
- Erro — Protocol error message
- PReq / PRes — Directory Server preparation request (card range negotiation)
- RReq / RRes — Results request for decoupled and app-based authentication
- JWE envelope — EMVCo-mandated end-to-end encryption for
acctNumberandsdkEncDatafields - DS certificate management — JWK / PKCS#12 signing for AReq integrity
This crate targets the EMVCo 3DS Core Specification v2.2.0 and v2.3.0. The spec is available (registration required) at emvco.com.
EMVCo 3DS is an open standard with no royalty requirements on implementations.
MIT — see LICENSE.