Project 4: 3D Secure Authentication Flow
Goal: Build practical expertise in payment security by implementing core controls (validation, tokenization, encryption), understanding PCI scope, and producing auditable, compliant artifacts.
Payment Data Boundaries
Payment systems live or die by data boundaries: where PANs can exist, how they move, and who can touch them. You need to draw a clear boundary between the Cardholder Data Environment (CDE) and everything else to reduce scope and risk.
Cryptographic Controls and Key Management
Payments rely on strong symmetric encryption, deterministic tokenization, and strict key lifecycle controls. Key custody, rotation, and HSM-backed operations are as important as the algorithms themselves.
Transaction Flow and Compliance Guarantees
Authorization, capture, and settlement have different security requirements. Compliance (PCI DSS, PCI PIN, 3DS) enforces minimal guarantees that must be reflected in system design.
Concept Summary Table
| Concept Cluster | What You Need to Internalize |
|---|---|
| Data classification | PAN vs token, CDE boundaries, data minimization. |
| Cryptography | AES, KDFs, tokenization, key hierarchy. |
| Transaction security | Auth vs settlement, 3DS, P2PE. |
| Compliance | PCI DSS scope, audit controls, evidence. |
| Risk controls | Rate limits, fraud signals, logging. |
Deep Dive Reading by Concept
| Concept | Book & Chapter |
|---|---|
| PCI DSS | PCI DSS v4.0 — Requirements overview |
| Tokenization | PCI Tokenization Guidelines — Implementation sections |
| Crypto in payments | Cryptography Engineering — Ch. 6-9 |
| Payment flows | Payment Systems in the U.S. — transaction chapters |
| Fraud controls | The Anatomy of the Payment Card Industry — risk sections |
Project Overview
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 2 weeks |
| Programming Language | C / Web (HTML/JS for frontend simulation) |
| Knowledge Area | Payment Protocols / Authentication |
| Key Technologies | 3D Secure 2.0, EMVCo Specs, Digital Signatures |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | 3. The “Service & Support” Model |
Learning Objectives
By completing this project, you will:
- Understand the 3D Secure protocol - Learn how cardholder authentication works for online payments
- Implement the three-party model - Build merchant, issuer, and directory server components
- Handle challenge and frictionless flows - Implement risk-based authentication decisions
- Verify cryptographic assertions - Validate signatures between parties
- Manage redirect flows securely - Prevent session hijacking and CSRF
- Understand liability shift - Learn how authentication affects fraud liability
The Core Question You’re Answering
“How can a bank verify that the person making an online purchase actually owns the card?”
Unlike in-store transactions where you physically present the card, online transactions have no way to verify cardholder presence. 3D Secure solves this:
┌─────────────────────────────────────────────────────────────────────────┐
│ THE CARD-NOT-PRESENT PROBLEM │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ IN-STORE TRANSACTION ONLINE TRANSACTION │
│ ════════════════════ ══════════════════ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Customer │ │ Customer │ │
│ │ with card │ │ (remote) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ Presents card │ Types card number │
│ │ Enters PIN │ ...that's it? │
│ │ Signs receipt │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Terminal │ │ Website │ │
│ │ verifies: │ │ has: │ │
│ │ • Card │ │ • PAN │ │
│ │ • PIN │ │ • CVV │ │
│ │ • Chip │ │ • Expiry │ │
│ │ • Signature│ │ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Authentication: Strong Authentication: WEAK! │
│ (something you have + (something you know + │
│ something you know) ...nothing else) │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ 3D SECURE ADDS: │
│ ═══════════════ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Customer │ │ Merchant │ │ Issuer │ │
│ │ │◄────►│ (website) │◄────►│ (bank) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ │ │ │
│ │ Challenge (if needed) │ │
│ └──────────────────────────────────────────►│ │
│ OTP, biometric, app notification │ │
│ │
│ Now the ISSUER verifies the cardholder! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The Security Model: 3D Secure lets the card-issuing bank authenticate its own customer, rather than trusting the merchant.
Deep Theoretical Foundation
1. The Three Domains
“3D” refers to Three Domains:
┌─────────────────────────────────────────────────────────────────────────┐
│ THREE DOMAIN MODEL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ DOMAIN 1: ISSUER DOMAIN │
│ ═══════════════════════ │
│ • Card-issuing bank │
│ • Access Control Server (ACS) │
│ • Authenticates the cardholder │
│ • Example: Your bank (Chase, BofA, etc.) │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ DOMAIN 2: ACQUIRER DOMAIN │
│ ═════════════════════════ │
│ • Merchant's payment processor │
│ • 3DS Server (MPI - Merchant Plug-In) │
│ • Initiates authentication │
│ • Example: Stripe, Square, Adyen │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ DOMAIN 3: INTEROPERABILITY DOMAIN │
│ ═════════════════════════════════ │
│ • Card network infrastructure │
│ • Directory Server (DS) │
│ • Routes messages between domains │
│ • Example: Visa, Mastercard │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ISSUER INTEROPERABILITY ACQUIRER │ │
│ │ DOMAIN DOMAIN DOMAIN │ │
│ │ │ │
│ │ ┌───────┐ ┌───────┐ ┌───────────┐ │ │
│ │ │ ACS │◄───────►│ DS │◄──────────►│ 3DS Server│ │ │
│ │ │ │ │ │ │ (MPI) │ │ │
│ │ └───┬───┘ └───────┘ └─────┬─────┘ │ │
│ │ │ │ │ │
│ │ │ Challenge Flow │ │ │
│ │ │◄──────────────────────────────────────►│ │ │
│ │ │ │ │ │
│ │ ┌───▼───┐ ┌─────▼─────┐ │ │
│ │ │Issuer │ │ Merchant │ │ │
│ │ │ Bank │ │ Website │ │ │
│ │ └───────┘ └───────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2. 3D Secure 2.0 vs 1.0
3D Secure 1.0 (Deprecated):
- Every transaction required a redirect
- Full-page redirect to bank’s authentication page
- Poor mobile experience
- High cart abandonment
3D Secure 2.0 (Current):
- Risk-based authentication
- Frictionless flow for low-risk transactions
- Embedded challenge (iframe, not redirect)
- Mobile SDK support
- Better data sharing for risk decisions
┌─────────────────────────────────────────────────────────────────────────┐
│ 3DS 1.0 vs 3DS 2.0 FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 3DS 1.0 (old) 3DS 2.0 (current) │
│ ═════════════ ═════════════════ │
│ │
│ Every transaction: Risk assessment first: │
│ │
│ Customer → Merchant → Redirect → Customer → Merchant → DS → │
│ Bank Page → Password → Redirect → │ │
│ Merchant ├─► Low risk? Frictionless │
│ │ (no customer action) │
│ │ │
│ └─► High risk? Challenge │
│ (OTP, biometric, etc.) │
│ │
│ Friction: 100% Friction: ~5% (typical) │
│ Abandonment: High Abandonment: Low │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3. The Authentication Request (AReq)
The merchant (via 3DS Server) sends rich data to the Directory Server:
┌─────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION REQUEST (AReq) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ REQUIRED FIELDS: │
│ ════════════════ │
│ • messageType: "AReq" │
│ • messageVersion: "2.2.0" │
│ • threeDSServerTransID: unique transaction ID │
│ • acctNumber: PAN (encrypted) │
│ • cardExpiryDate: "2512" (YYMM) │
│ • purchaseAmount: "10000" (in minor units) │
│ • purchaseCurrency: "840" (USD) │
│ • merchantName: "Example Store" │
│ • deviceChannel: "02" (browser) / "01" (app) │
│ │
│ RISK ASSESSMENT DATA: │
│ ═════════════════════ │
│ • browserAcceptHeader: from HTTP request │
│ • browserIP: customer's IP address │
│ • browserLanguage: "en-US" │
│ • browserUserAgent: full user agent string │
│ • browserJavaEnabled: true/false │
│ • browserColorDepth: "24" │
│ • browserScreenHeight: "1080" │
│ • browserScreenWidth: "1920" │
│ • browserTZ: "-300" (timezone offset) │
│ │
│ • cardholderEmail: "customer@example.com" │
│ • cardholderName: "John Doe" │
│ • billAddrLine1, billAddrCity, billAddrPostCode, billAddrCountry │
│ • shipAddrLine1, shipAddrCity, shipAddrPostCode, shipAddrCountry │
│ │
│ • threeDSRequestorAuthenticationInd: "01" (payment) │
│ • transType: "01" (goods/service purchase) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4. Risk-Based Authentication
The ACS (issuer) analyzes the AReq data to decide:
┌─────────────────────────────────────────────────────────────────────────┐
│ RISK-BASED DECISION MATRIX │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ SIGNALS FOR FRICTIONLESS (low risk): │
│ ═══════════════════════════════════ │
│ • Known device (browser fingerprint matches history) │
│ • Known IP range (customer's usual location) │
│ • Low transaction amount │
│ • Shipping to billing address │
│ • Trusted merchant │
│ • Recent successful authentication │
│ │
│ SIGNALS FOR CHALLENGE (high risk): │
│ ═════════════════════════════════ │
│ • New device │
│ • Unusual location/IP │
│ • High transaction amount │
│ • Different shipping address │
│ • Unusual purchase time │
│ • Card recently compromised (data breach) │
│ • Multiple failed attempts │
│ │
│ DECISION MATRIX: │
│ ═══════════════ │
│ │
│ Low Amount │ High Amount │
│ ────────────┼──────────── │
│ Known │ ✓ Frictionless │ ? Maybe Challenge │
│ Device │ │ │
│ ────────────┼──────────── │
│ New │ ? Maybe │ ✗ Challenge Required │
│ Device │ Challenge │ │
│ ────────────┼──────────── │
│ │
└─────────────────────────────────────────────────────────────────────────┘
5. Authentication Response (ARes)
┌─────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION RESPONSE (ARes) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ FRICTIONLESS APPROVAL: │
│ ═════════════════════ │
│ { │
│ "messageType": "ARes", │
│ "transStatus": "Y", // Authenticated │
│ "transStatusReason": null, │
│ "eci": "05", // Fully authenticated │
│ "authenticationValue": "CAVV...", // Cryptographic proof │
│ "acsTransID": "uuid" │
│ } │
│ │
│ CHALLENGE REQUIRED: │
│ ══════════════════ │
│ { │
│ "messageType": "ARes", │
│ "transStatus": "C", // Challenge required │
│ "acsChallengeMandated": "Y", │
│ "acsURL": "https://acs.issuer.com/challenge", │
│ "acsTransID": "uuid", │
│ "authenticationType": "01" // OTP │
│ } │
│ │
│ FAILED/REJECTED: │
│ ═══════════════ │
│ { │
│ "messageType": "ARes", │
│ "transStatus": "N", // Not authenticated │
│ "transStatusReason": "01", // Card not enrolled │
│ "eci": "07" // Attempted (no liability shift) │
│ } │
│ │
│ TRANSACTION STATUS VALUES: │
│ ══════════════════════════ │
│ Y = Authenticated │
│ N = Not authenticated / Authentication failed │
│ U = Authentication could not be performed │
│ A = Attempted (ACS not available) │
│ C = Challenge required │
│ R = Rejected by issuer │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6. ECI (E-Commerce Indicator) Values
┌─────────────────────────────────────────────────────────────────────────┐
│ ECI VALUES │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ VISA: MASTERCARD: │
│ • 05: Fully authenticated • 02: Fully authenticated │
│ • 06: Attempted authentication • 01: Attempted authentication │
│ • 07: No authentication • 00: No authentication │
│ │
│ LIABILITY SHIFT: │
│ ════════════════ │
│ │
│ ECI 05/02 (Authenticated): │
│ • Liability shifts to ISSUER │
│ • Merchant protected from chargebacks (fraud) │
│ • Best outcome for merchant │
│ │
│ ECI 06/01 (Attempted): │
│ • Partial liability shift in some cases │
│ • Card enrolled but ACS unavailable │
│ │
│ ECI 07/00 (No Authentication): │
│ • No liability shift │
│ • Merchant bears fraud risk │
│ • Card not enrolled or merchant opted out │
│ │
└─────────────────────────────────────────────────────────────────────────┘
7. The Challenge Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ CHALLENGE FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Customer │ │ Merchant │ │ DS │ │ ACS │ │
│ │ Browser │ │ 3DS │ │ │ │ (Issuer) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ │ Checkout │ │ │ │
│ │───────────────►│ │ │ │
│ │ │ AReq │ │ │
│ │ │───────────────►│ │ │
│ │ │ │ AReq │ │
│ │ │ │───────────────►│ │
│ │ │ │ │ │
│ │ │ │ ARes │ │
│ │ │ │◄───────────────│ │
│ │ │ ARes │ (C=Challenge) │
│ │ │◄───────────────│ │ │
│ │ │ │ │ │
│ │ Challenge │ │ │ │
│ │ (iframe) │ │ │ │
│ │◄───────────────│ │ │ │
│ │ │ │ │ │
│ ├──────────────────────────────────────────────────► │
│ │ CReq (challenge request) │ │
│ │ │ │
│ │◄────────────────────────────────────────────────── │
│ │ Challenge UI (OTP form) │ │
│ │ │ │
│ ├──────────────────────────────────────────────────► │
│ │ CRes (OTP entered) │ │
│ │ │ │
│ │◄────────────────────────────────────────────────── │
│ │ RReq (result) │ │
│ │ │◄────────────────────────────────│ │
│ │ │ │ │ │
│ │ Result │ │ │ │
│ │◄───────────────│ │ │ │
│ │ │ │ │ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Project Specification
What You’ll Build
A working 3D Secure 2.0 simulation including:
- Merchant 3DS Server: Initiates authentication
- Directory Server (Mock): Routes between merchant and issuer
- Access Control Server (Mock): Makes authentication decisions
- Checkout Page: Demonstrates the customer experience
- Challenge Interface: OTP/password challenge flow
Expected Output
╔═══════════════════════════════════════════════════════════════════════╗
║ 3D SECURE DEMO CHECKOUT ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ Purchase Details ║
║ ─────────────── ║
║ Item: "Wireless Headphones" ║
║ Amount: $149.99 ║
║ Merchant: Demo Store ║
║ ║
║ Card Information ║
║ ─────────────── ║
║ Card Number: 4111111111111111 ║
║ Expiry: 12/25 ║
║ CVV: 123 ║
║ Name: John Doe ║
║ ║
║ [PAY NOW] ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
[User clicks PAY NOW]
╔═══════════════════════════════════════════════════════════════════════╗
║ 3DS AUTHENTICATION IN PROGRESS ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ [3DS Server] Collecting browser data... ║
║ [3DS Server] Browser: Chrome 121.0.0.0 ║
║ [3DS Server] IP: 192.168.1.100 ║
║ [3DS Server] Screen: 1920x1080 ║
║ ║
║ [3DS Server] Sending AReq to Directory Server... ║
║ { ║
║ "messageType": "AReq", ║
║ "acctNumber": "4111111111111111", ║
║ "purchaseAmount": "14999", ║
║ "browserIP": "192.168.1.100" ║
║ } ║
║ ║
║ [DS] Received AReq, routing to ACS for BIN 411111... ║
║ ║
║ [ACS] Risk Assessment: ║
║ • Known device: NO ║
║ • Known IP: NO ║
║ • Amount: $149.99 (MEDIUM) ║
║ • Risk Score: 72/100 ║
║ • Decision: CHALLENGE REQUIRED ║
║ ║
║ [ACS] Sending ARes with transStatus="C" ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════╗
║ CHALLENGE: Verify Your Identity ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌─────────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ 🏦 Your Bank │ ║
║ │ │ ║
║ │ To verify this purchase, we've sent a one-time │ ║
║ │ password to your registered phone ending in **89 │ ║
║ │ │ ║
║ │ Purchase: $149.99 at Demo Store │ ║
║ │ │ ║
║ │ Enter OTP: [______] │ ║
║ │ │ ║
║ │ [VERIFY] [Cancel] │ ║
║ │ │ ║
║ │ Didn't receive code? Resend │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────────┘ ║
║ ║
║ (This iframe is served by the issuer's ACS) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
[User enters correct OTP: 123456]
╔═══════════════════════════════════════════════════════════════════════╗
║ AUTHENTICATION COMPLETE ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ [ACS] OTP verified successfully ║
║ [ACS] Generating authentication value (CAVV)... ║
║ [ACS] Sending RReq to 3DS Server ║
║ ║
║ Authentication Result: ║
║ ───────────────────── ║
║ Status: Y (AUTHENTICATED) ║
║ ECI: 05 (Visa fully authenticated) ║
║ CAVV: AAABBJlHYWUZ... (base64) ║
║ ║
║ LIABILITY SHIFT: ✓ Issuer accepts fraud liability ║
║ ║
║ [3DS Server] Authentication successful ║
║ [3DS Server] Proceeding with authorization... ║
║ ║
║ ═══════════════════════════════════════════════════════════════════ ║
║ ║
║ ✓ PAYMENT APPROVED ║
║ ║
║ Authorization Code: A12345 ║
║ Transaction ID: TXN-987654321 ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
Project Structure
3ds_simulator/
├── src/
│ ├── main.c # CLI/server entry
│ ├── merchant/
│ │ ├── checkout.c # Checkout page handler
│ │ ├── threeds_server.c # 3DS Server (MPI)
│ │ └── browser_data.c # Browser fingerprinting
│ ├── directory_server/
│ │ ├── ds.c # Directory Server
│ │ ├── routing.c # BIN to ACS routing
│ │ └── message_validation.c # Schema validation
│ ├── acs/
│ │ ├── acs.c # Access Control Server
│ │ ├── risk_engine.c # Risk-based decision
│ │ ├── challenge.c # Challenge flow
│ │ └── otp.c # OTP generation/validation
│ ├── protocol/
│ │ ├── areq.c # AReq message
│ │ ├── ares.c # ARes message
│ │ ├── creq.c # CReq message
│ │ ├── cres.c # CRes message
│ │ └── rreq.c # RReq message
│ ├── crypto/
│ │ ├── signatures.c # Message signatures
│ │ └── cavv.c # CAVV generation
│ └── web/
│ ├── http_server.c # Basic HTTP server
│ └── json.c # JSON parsing
├── static/
│ ├── checkout.html # Checkout page
│ ├── challenge.html # Challenge iframe
│ └── style.css
├── tests/
│ ├── test_areq.c
│ ├── test_risk_engine.c
│ └── test_challenge.c
├── Makefile
└── README.md
Core API Design
// threeds_server.h
typedef struct {
char trans_id[37]; // UUID
char merchant_id[32];
char merchant_name[64];
int purchase_amount; // In minor units (cents)
char purchase_currency[4]; // ISO currency code
} TransactionInfo;
typedef struct {
char acct_number[20];
char card_expiry[5]; // YYMM
char cardholder_name[64];
} CardInfo;
typedef struct {
char user_agent[256];
char accept_header[128];
char ip_address[46];
char language[8];
int screen_width;
int screen_height;
int color_depth;
int timezone_offset;
bool java_enabled;
bool javascript_enabled;
} BrowserInfo;
// Build and send AReq
typedef struct {
char trans_status; // Y, N, C, U, A, R
char eci[3]; // 05, 06, 07
char cavv[64]; // Base64 encoded
char acs_url[256]; // For challenge
char acs_trans_id[37];
bool challenge_required;
} AResResult;
AResResult initiate_authentication(
const TransactionInfo* trans,
const CardInfo* card,
const BrowserInfo* browser
);
// acs.h
typedef struct {
int risk_score; // 0-100
char decision; // Y (frictionless), C (challenge), N (deny)
char reason[64];
} RiskDecision;
RiskDecision assess_risk(
const char* pan,
int amount,
const BrowserInfo* browser,
const char* merchant_id
);
typedef struct {
char otp[7]; // 6 digits
time_t expires_at;
char trans_id[37];
} Challenge;
Challenge generate_challenge(const char* trans_id, const char* phone_last4);
bool verify_challenge(const char* trans_id, const char* otp_entered);
Solution Architecture
System Design
┌─────────────────────────────────────────────────────────────────────────┐
│ 3DS SIMULATOR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ BROWSER │
│ ═══════ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ checkout.html │ │
│ │ │ │
│ │ • Card form │ │
│ │ • Browser data collection (JS) │ │
│ │ • 3DS Method iframe (device fingerprinting) │ │
│ │ • Challenge iframe container │ │
│ │ │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ │ POST /initiate-3ds │
│ │ {card, browser_data, transaction} │
│ ▼ │
│ MERCHANT SERVER (3DS Server) │
│ ════════════════════════════ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ threeds_server.c │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ Build AReq │ │ Parse ARes │ │ │
│ │ │ (protocol/areq.c) │ │ (protocol/ares.c) │ │ │
│ │ └─────────┬──────────┘ └──────────┬─────────┘ │ │
│ │ │ │ │ │
│ │ │ AReq JSON │ ARes JSON │ │
│ │ │ │ │ │
│ └─────────────┼─────────────────────────┼─────────────────────────┘ │
│ │ │ │
│ ▼ │ │
│ DIRECTORY SERVER │ │
│ ════════════════ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ds.c │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ Validate Message │ │ Route by BIN │ │ │
│ │ │ │ │ (BIN → ACS URL) │ │ │
│ │ └─────────┬──────────┘ └──────────┬─────────┘ │ │
│ │ │ │ │ │
│ │ │ AReq │ ARes │ │
│ │ ▼ │ │ │
│ └─────────────┼─────────────────────────┼─────────────────────────┘ │
│ │ │ │
│ ▼ │ │
│ ACCESS CONTROL SERVER (ACS) │ │
│ ═══════════════════════════ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ acs.c │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ Risk Engine │ │ Challenge Logic │ │ │
│ │ │ (risk_engine.c) │ │ (challenge.c) │ │ │
│ │ │ │ │ │ │ │
│ │ │ • Device history │ │ • OTP generation │ │ │
│ │ │ • Amount analysis │ │ • OTP validation │ │ │
│ │ │ • Behavioral │ │ • Timeout │ │ │
│ │ └─────────┬──────────┘ └──────────┬─────────┘ │ │
│ │ │ │ │ │
│ │ ▼ │ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ Decision: │ │ │
│ │ │ • Frictionless (transStatus=Y) → Return ARes │ │ │
│ │ │ • Challenge (transStatus=C) → Serve challenge UI │ │ │
│ │ │ • Deny (transStatus=N) → Return ARes with reason │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ CAVV Generation (crypto/cavv.c) │ │ │
│ │ │ • Sign authentication result │ │ │
│ │ │ • Include transaction details │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Risk Engine Design
// risk_engine.c
typedef struct {
// Device fingerprint
char device_id[64]; // Hash of browser characteristics
// History
int previous_purchases; // From this device
int failed_attempts; // Recent failures
time_t last_purchase;
// Current transaction
int amount;
bool shipping_matches_billing;
char merchant_category[8]; // MCC code
} RiskFactors;
#define RISK_WEIGHT_DEVICE_NEW 25
#define RISK_WEIGHT_IP_NEW 15
#define RISK_WEIGHT_HIGH_AMOUNT 20
#define RISK_WEIGHT_DIFFERENT_SHIP 10
#define RISK_WEIGHT_ODD_TIME 5
#define RISK_WEIGHT_FAILED_RECENT 15
#define RISK_WEIGHT_SUSPICIOUS_MCC 10
int calculate_risk_score(const RiskFactors* factors) {
int score = 0;
// New device
if (factors->previous_purchases == 0) {
score += RISK_WEIGHT_DEVICE_NEW;
}
// High amount (threshold varies by merchant)
if (factors->amount > 10000) { // $100 in cents
score += RISK_WEIGHT_HIGH_AMOUNT;
}
// Different shipping
if (!factors->shipping_matches_billing) {
score += RISK_WEIGHT_DIFFERENT_SHIP;
}
// Recent failures
if (factors->failed_attempts > 0) {
score += RISK_WEIGHT_FAILED_RECENT * factors->failed_attempts;
}
return score > 100 ? 100 : score;
}
RiskDecision assess_risk(/*...*/) {
RiskFactors factors = gather_factors(/*...*/);
int score = calculate_risk_score(&factors);
RiskDecision decision;
decision.risk_score = score;
if (score < 30) {
decision.decision = 'Y'; // Frictionless
strcpy(decision.reason, "Low risk transaction");
} else if (score < 70) {
decision.decision = 'C'; // Challenge
strcpy(decision.reason, "Moderate risk - verification needed");
} else {
decision.decision = 'N'; // Deny
strcpy(decision.reason, "High risk - declined");
}
return decision;
}
Implementation Guide
Phase 1: Protocol Messages
Goal: Implement AReq/ARes message structures.
// areq.c
#include <json.h>
typedef struct {
char message_type[8]; // "AReq"
char message_version[8]; // "2.2.0"
char three_ds_server_trans_id[37];
char acct_number[20];
char card_expiry_date[5];
char purchase_amount[16];
char purchase_currency[4];
char merchant_name[64];
char device_channel[3];
// Browser info
char browser_accept_header[256];
char browser_ip[46];
char browser_language[8];
char browser_user_agent[512];
// ... many more fields
} AReq;
char* areq_to_json(const AReq* areq) {
json_t* root = json_object();
json_object_set_new(root, "messageType",
json_string(areq->message_type));
json_object_set_new(root, "messageVersion",
json_string(areq->message_version));
// ... all fields
char* json_str = json_dumps(root, JSON_COMPACT);
json_decref(root);
return json_str;
}
Phase 2: Basic Server
Goal: HTTP server to handle checkout flow.
// Using mongoose or libmicrohttpd
void handle_initiate_3ds(Request* req, Response* res) {
// Parse card, browser data, transaction from request
CardInfo card;
BrowserInfo browser;
TransactionInfo trans;
parse_checkout_request(req->body, &card, &browser, &trans);
// Build AReq
AReq areq = build_areq(&card, &browser, &trans);
// Send to DS (in production, over TLS mutual auth)
AResResult ares = send_areq_to_ds(&areq);
if (ares.challenge_required) {
// Return challenge URL to browser
json_response(res, 200, "{\"challenge\": true, \"acsUrl\": \"%s\"}",
ares.acs_url);
} else if (ares.trans_status == 'Y') {
// Proceed with authorization
AuthResult auth = authorize_transaction(&trans, ares.cavv, ares.eci);
json_response(res, 200, "{\"success\": true, \"authCode\": \"%s\"}",
auth.auth_code);
} else {
json_response(res, 200, "{\"success\": false, \"reason\": \"%s\"}",
ares.reason);
}
}
Phase 3: Directory Server Routing
Goal: Route AReq to correct ACS by BIN.
// routing.c
typedef struct {
char bin_start[9];
char bin_end[9];
char acs_url[256];
char acs_id[32];
} BinRoute;
static BinRoute routes[] = {
{"40000000", "49999999", "http://localhost:8082/acs", "VISA_ACS_001"},
{"51000000", "55999999", "http://localhost:8083/acs", "MC_ACS_001"},
{"34000000", "34999999", "http://localhost:8084/acs", "AMEX_ACS_001"},
// ...
};
const char* route_to_acs(const char* pan) {
char bin[9];
strncpy(bin, pan, 8);
bin[8] = '\0';
for (int i = 0; i < sizeof(routes)/sizeof(routes[0]); i++) {
if (strcmp(bin, routes[i].bin_start) >= 0 &&
strcmp(bin, routes[i].bin_end) <= 0) {
return routes[i].acs_url;
}
}
return NULL; // Card not enrolled
}
Phase 4: ACS Risk Engine
Goal: Make frictionless vs challenge decisions.
See Risk Engine Design section above.
Phase 5: Challenge Flow
Goal: Implement OTP challenge.
// challenge.c
// In-memory challenge store (use Redis in production)
static Challenge active_challenges[1000];
static int challenge_count = 0;
Challenge generate_challenge(const char* trans_id, const char* phone_last4) {
Challenge c;
strncpy(c.trans_id, trans_id, 36);
c.trans_id[36] = '\0';
// Generate 6-digit OTP
unsigned int seed;
RAND_bytes((unsigned char*)&seed, sizeof(seed));
snprintf(c.otp, 7, "%06d", seed % 1000000);
c.expires_at = time(NULL) + 300; // 5 minutes
// Store
active_challenges[challenge_count++] = c;
// In production: actually send SMS via Twilio, etc.
printf("[ACS] Sending OTP %s to phone ending in %s\n", c.otp, phone_last4);
return c;
}
bool verify_challenge(const char* trans_id, const char* otp_entered) {
for (int i = 0; i < challenge_count; i++) {
if (strcmp(active_challenges[i].trans_id, trans_id) == 0) {
if (time(NULL) > active_challenges[i].expires_at) {
return false; // Expired
}
return strcmp(active_challenges[i].otp, otp_entered) == 0;
}
}
return false; // Not found
}
Phase 6: CAVV Generation
Goal: Generate cryptographic authentication value.
// cavv.c
// CAVV structure (simplified - real CAVV is more complex)
typedef struct {
char algorithm; // 0 = HMAC, 1 = CVN etc.
char version; // Protocol version
char key_indicator[2]; // Which key was used
char auth_result; // Y/A/N etc.
char trans_id[4]; // Abbreviated trans ID
char mac[20]; // HMAC of above
} CAVV;
char* generate_cavv(const char* trans_id, char auth_result,
const unsigned char* key) {
CAVV cavv;
cavv.algorithm = 0;
cavv.version = 2;
cavv.auth_result = auth_result;
// Copy first 4 bytes of trans ID
memcpy(cavv.trans_id, trans_id, 4);
// Generate HMAC
unsigned char to_mac[8];
to_mac[0] = cavv.algorithm;
to_mac[1] = cavv.version;
to_mac[2] = cavv.auth_result;
memcpy(to_mac + 3, cavv.trans_id, 4);
HMAC(EVP_sha1(), key, 16, to_mac, 7, cavv.mac, NULL);
// Base64 encode entire structure
char* b64 = base64_encode((unsigned char*)&cavv, sizeof(cavv));
return b64;
}
Testing Strategy
Protocol Compliance Tests
void test_areq_required_fields() {
AReq areq = {0};
// Missing required field
char* json = areq_to_json(&areq);
// Should fail validation
assert(validate_areq(json) == false);
// Add required fields
strcpy(areq.message_type, "AReq");
strcpy(areq.message_version, "2.2.0");
// ... add all required
json = areq_to_json(&areq);
assert(validate_areq(json) == true);
}
Risk Engine Tests
void test_low_risk_frictionless() {
RiskFactors factors = {
.previous_purchases = 10,
.failed_attempts = 0,
.amount = 1000, // $10
.shipping_matches_billing = true
};
int score = calculate_risk_score(&factors);
assert(score < 30); // Should be frictionless
}
void test_high_risk_challenge() {
RiskFactors factors = {
.previous_purchases = 0, // New device
.failed_attempts = 2,
.amount = 50000, // $500
.shipping_matches_billing = false
};
int score = calculate_risk_score(&factors);
assert(score >= 30); // Should require challenge
}
End-to-End Flow Tests
#!/bin/bash
# test_3ds_flow.sh
# Start all servers
./ds_server &
./acs_server &
./merchant_server &
sleep 2
# Test frictionless flow (low amount, known device)
RESPONSE=$(curl -s -X POST http://localhost:8080/initiate-3ds \
-H "Content-Type: application/json" \
-d '{
"pan": "4111111111111111",
"amount": 1000,
"browserInfo": {...}
}')
assert_contains "$RESPONSE" '"transStatus":"Y"'
# Test challenge flow (high amount)
RESPONSE=$(curl -s -X POST http://localhost:8080/initiate-3ds \
-H "Content-Type: application/json" \
-d '{
"pan": "4111111111111111",
"amount": 100000,
"browserInfo": {...}
}')
assert_contains "$RESPONSE" '"challenge":true'
Common Pitfalls & Debugging
Pitfall 1: Incorrect Version Handling
Symptom: DS rejects messages.
Cause: Protocol version mismatch between components.
Fix: Ensure all components use same messageVersion.
Pitfall 2: CAVV Validation Failure
Symptom: Authorization fails despite successful authentication.
Cause: CAVV format doesn’t match network expectations.
Fix: Follow EMVCo specification exactly for CAVV structure.
Pitfall 3: Challenge Iframe Issues
Symptom: Challenge doesn’t display.
Cause: CSP blocking iframe, or cross-origin issues.
Fix: Set proper headers: X-Frame-Options: ALLOW-FROM merchant.com.
Extensions & Challenges
Extension 1: Device Fingerprinting
Implement 3DS Method for device fingerprinting before authentication.
Extension 2: SCA Exemptions
Implement Strong Customer Authentication exemptions (low value, recurring, etc.)
Extension 3: Decoupled Authentication
Implement app-based push notification authentication.
Extension 4: Real Card Network Integration
Integrate with Visa/Mastercard test environments.
Resources
Specifications
- EMVCo 3D Secure 2.x Specification (free from emvco.com)
- EMVCo 3D Secure SDK Specification
- PCI 3DS Core Security Standard
Books
| Topic | Book |
|---|---|
| Digital Signatures | Serious Cryptography Ch. 13 |
| Web Security | Bug Bounty Bootcamp by Vickie Li |
| Risk-Based Auth | Security in Computing by Pfleeger |
Self-Assessment Checklist
- AReq/ARes messages comply with EMVCo spec
- Risk engine makes reasonable decisions
- Challenge flow works end-to-end
- CAVV is generated correctly
- Frictionless flow works for low-risk transactions
- Can explain liability shift
- Understand the three-domain model
- Iframe security is properly handled
What’s Next?
You now understand e-commerce authentication. Move to Project 5: Mini Payment Gateway to build a system that ties together tokenization, P2PE, and 3DS with PCI-compliant architecture.