Skip to content

copyleftdev/emv-3ds

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Crates.io docs.rs License: MIT Rust 1.75+

*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 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

Features

  • All nine protocol messagesAReq, 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 AwaitingAResAwaitingCRes / AwaitingRReqAuthenticated / NotAuthenticated / Failed, with invalid-transition errors.
  • Card range negotiationPreparationResponse::range_for_pan() to look up the card range entry (including threeDSMethodURL) for a given PAN prefix.
  • Decoupled authenticationAwaitingRReq state, receive_rreq() transition, and ResultsResponse::acknowledge() builder for the ACS callback flow.
  • Coded value enumsTransStatus, Eci, ChallengeIndicator, MessageVersion, TransStatusReason (21 codes), ActionIndicator, AcsAuthMethod, AuthenticationType, ResultsStatus, and all other spec enumerations.
  • ECI / liability shift helpersEci::has_liability_shift(), TransStatus::is_authenticated(), AuthenticationResponse::requires_challenge().
  • ISO 4217 currencyCurrency newtype with zero-padded spec string, Amount with spec_amount / spec_currency / spec_exponent EMVCo field getters.
  • Message envelopeMessage 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

[dependencies]
emv-3ds = "0.2"

Build and send an AReq

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

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

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

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

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

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

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

Roadmap

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

EMVCo 3DS is an open standard with no royalty requirements on implementations.


License

MIT — see LICENSE.

About

EMV 3-D Secure 2.x (3DS2) protocol — AReq/ARes/CReq/CRes messages, transaction state machine, ECI/CAVV helpers, and full EMVCo spec serialization for payment authentication (SCA/PSD2)

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors