Skip to content

Apple Pay (merchant tự giải mã) ​

Một biến thể của luồng Apple Pay dành cho các merchant tự giữ private key Payment Processing của riêng mình. Thay vì gửi cho Bahn PKPaymentToken đã mã hóa, bạn tự giải mã nó và xác nhận PaymentIntent bằng các trường device-token dạng plaintext.

Mọi thứ cho đến thời điểm PassKit trả về token đều giống hệt luồng cơ bản — cùng một Merchant ID, cùng một PKPaymentRequest, cùng một payment sheet. Chỉ có những gì backend của bạn làm với token là thay đổi.

Tôi đang dùng biến thể nào?

Apple PayTrang này
Ai giữ private key Payment ProcessingBahnBạn
Ai tạo CSRBahnBạn
Backend của bạn gửi gìToken đã mã hóaCác trường đã giải mã
Trường trong requestapple_pay.tokenapple_pay.decrypted_token
Gánh nặng PCI / quản lý khóaCủa BahnCủa bạn

Chỉ chọn biến thể này nếu bạn đã vận hành sẵn một HSM hoặc KMS và muốn token plaintext luôn nằm trong vành đai bảo mật của riêng bạn. Nó đòi hỏi nhiều công sức và rủi ro hơn; luồng cơ bản tồn tại vì hầu hết các tích hợp không cần đến điều này.

Điều kiện tiên quyết ​

Thiết lập phía Apple chỉ khác luồng cơ bản đúng một dòng:

Yêu cầuỞ đâuGhi chú
Merchant IDApple Developer → Identifiers → Merchant IDsKhông đổi. Được dùng làm PKPaymentRequest.merchantIdentifier.
Payment Processing CertificateApple Developer → Merchant ID của bạnĐược tạo từ một CSR do bạn tự tạo. Bạn giữ private key tương ứng, và bạn là bên duy nhất có thể giải mã token. Không yêu cầu Bahn cấp CSR cho biến thể này.
Apple Pay entitlementXcode → Signing & CapabilitiesKhông đổi.

WARNING

Private key Payment Processing là thứ duy nhất đứng giữa một token bị đánh cắp và một PAN thiết bị cùng cryptogram có thể sử dụng được. Hãy tạo nó bên trong một HSM hoặc KMS (P-256 cho token EC_v1), không bao giờ export nó, và chỉ cấp quyền sử dụng cho service xác nhận của bạn. Mất nó nghĩa là phải cấp lại chứng chỉ; để lộ nó nghĩa là kẻ tấn công có thể đọc mọi token mà Apple từng mã hóa bằng nó.

INFO

Hãy báo Merchant ID của bạn cho Bahn trong quá trình onboarding để các giao dịch ví điện tử được đối chiếu chính xác. Với biến thể này, Bahn không cần chứng chỉ Payment Processing của bạn — Bahn không bao giờ nhìn thấy token đã mã hóa.

Luồng tổng quan ​

sequenceDiagram
    autonumber
    participant User as User
    participant PassKit as PassKit<br/>(iOS / Secure Element)
    participant App as Merchant App<br/>(iOS)
    participant MerchantAPI as Merchant API<br/>(Backend)
    participant API as Bahn API

    Note over User, API: Phase 1: Create PaymentIntent
    User ->> App: Tap "Buy with Apple Pay"
    App ->> MerchantAPI: Request checkout
    MerchantAPI ->> API: CreatePaymentIntent(amount, currency=704)
    API -->> MerchantAPI: {payment_intent, client_secret}
    MerchantAPI -->> App: {order_id}

    Note over User, API: Phase 2: Authorise in PassKit
    App ->> PassKit: Present PKPaymentAuthorizationController<br/>(PKPaymentRequest)
    PassKit -->> User: Show sheet (card, amount, merchant)
    User ->> PassKit: Approve with Face ID / Touch ID
    PassKit -->> App: PKPayment.token.paymentData<br/>(encrypted JSON)

    rect rgb(255, 244, 250)
    Note over App, MerchantAPI: Phase 3: Decrypt — the delta
    App ->> MerchantAPI: POST /checkout/apple-pay {order_id, token}
    MerchantAPI ->> MerchantAPI: Verify Apple's signature
    MerchantAPI ->> MerchantAPI: Decrypt with Payment<br/>Processing private key (HSM)
    end

    Note over User, API: Phase 4: Confirm
    MerchantAPI ->> API: ConfirmPaymentIntent(id,<br/>payment_method_data.apple_pay.decrypted_token)
    API ->> API: Tokenize DPAN, submit to network
    API -->> MerchantAPI: payment_intent.status = "succeeded" | "processing"
    MerchantAPI -->> App: {status}
    App ->> PassKit: completion(.success)
    PassKit -->> User: Dismiss sheet with checkmark

    Note over MerchantAPI, API: Phase 5: Payment Result
    par Notify
        API ->> MerchantAPI: Webhook: payment_intent.status.succeeded
        MerchantAPI ->> MerchantAPI: Consume webhook event
    end

Các giai đoạn 1, 2, và 5 không đổi so với luồng cơ bản — xem Apple Pay để biết cách thiết lập PKPaymentRequest, delegate callback, và hợp đồng webhook. Phần còn lại của trang này mô tả giai đoạn 3 và 4.

WARNING

Bạn vẫn chỉ có khoảng 30 giây kể từ didAuthorizePayment để gọi completion handler, và việc giải mã giờ đây nằm trong khoảng thời gian đó. Hãy giữ kết nối HSM luôn sẵn sàng (warm). Nếu việc confirm có thể mất lâu hơn, hãy hoàn tất sheet với .success ngay khi Bahn trả về processing và đối chiếu sau qua webhook.


Giải mã token ​

Tài liệu chính thức của Apple là Payment Token Format Reference; phần này mô tả những gì bạn cần để đạt được decrypted_token cho Bahn.

1. Những gì PassKit trả về cho bạn ​

PKPaymentToken.paymentData là một envelope JSON:

json
{
  "data": "WTP8BxZNUs…",
  "signature": "MIAGCSqGSIb3…",
  "header": {
    "publicKeyHash": "7c4ZFn…",
    "ephemeralPublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…",
    "transactionId": "31323334353637383930"
  },
  "version": "EC_v1"
}
  • version là EC_v1 đối với chứng chỉ ECC (trường hợp thông thường) hoặc RSA_v1 đối với chứng chỉ RSA. Giá trị bạn nhận được phụ thuộc vào chứng chỉ bạn đã upload lên Apple.
  • header.publicKeyHash là SHA-256 của public key Payment Processing của bạn, được mã hóa base64. Dùng nó để chọn đúng key khi bạn xoay vòng chứng chỉ hoặc chạy nhiều hơn một key.
  • header.transactionId là định danh giao dịch do thiết bị tạo ra. Hãy giữ lại nó — Bahn yêu cầu giá trị này và nó không xuất hiện trong payload đã giải mã.

WARNING

Chuyển tiếp paymentData từ app dưới dạng chuỗi JSON UTF-8 mà PassKit tạo ra. Không bọc thêm base64, không serialize lại, và không sắp xếp lại các key — chữ ký của Apple bao trùm chính xác các byte gốc, và bất kỳ thay đổi nào cũng sẽ khiến việc xác minh thất bại.

2. Xác minh chữ ký ​

Trước khi giải mã, hãy xác minh chữ ký PKCS#7 tách rời (detached) trong signature:

  • Chuỗi chứng chỉ kết thúc tại Apple Root CA – G3.
  • Chứng chỉ leaf mang OID 1.2.840.113635.100.6.29; chứng chỉ trung gian mang 1.2.840.113635.100.6.2.14.
  • Chữ ký bao trùm phần nối của ephemeralPublicKey (hoặc wrappedKey đối với RSA_v1), data, transactionId, và applicationData (nếu có) — tất cả dưới dạng byte thô.
  • Từ chối một token có thời gian ký cách quá xa so với hiện tại.

3. Giải mã data ​

Đối với EC_v1:

  1. Thực hiện ECDH giữa private key Payment Processing của bạn và header.ephemeralPublicKey để tạo ra shared secret.
  2. Chạy hàm KDF nối chuỗi NIST SP 800-56A với SHA-256 trên shared secret đó: counter 0x00000001, AlgorithmID = 0x0D id-aes256-GCM, PartyUInfo = Apple, PartyVInfo = SHA-256 của merchant identifier của bạn (đọc từ extension 1.2.840.113635.100.6.32 của chứng chỉ).
  3. Giải mã AES-256-GCM cho data với IV 16-byte toàn số 0; 16 byte cuối của ciphertext là auth tag.

Đối với RSA_v1: dùng RSA-OAEP để unwrap header.wrappedKey bằng private key của bạn để lấy AES key, sau đó giải mã data bằng AES-256-GCM như trên.

DANGER

Plaintext là một PAN thiết bị thực và một cryptogram có thể tính phí. Không bao giờ log nó, không bao giờ lưu trữ nó, và không bao giờ trả nó về cho app. Giải mã nó, gửi cho Bahn, rồi loại bỏ nó.

4. Ánh xạ dữ liệu đã giải mã sang decrypted_token ​

Kết quả là dữ liệu thanh toán JSON của Apple:

json
{
  "applicationPrimaryAccountNumber": "4111111111111111",
  "applicationExpirationDate": "281231",
  "currencyCode": "704",
  "transactionAmount": 8000,
  "cardholderName": "Nguyen Van A",
  "deviceManufacturerIdentifier": "040010030273",
  "paymentDataType": "3DSecure",
  "paymentData": {
    "onlinePaymentCryptogram": "AoWXNg6NkAAAAAAAAAAAAAA=",
    "eciIndicator": "05"
  }
}

Tên trường của Bahn ánh xạ 1:1 với tên trường của Apple theo snake_case, vì vậy không cần bảng chuyển đổi:

Key của AppleTrường của BahnBắt buộcGhi chú
applicationPrimaryAccountNumberapplication_primary_account_numberCóPAN thiết bị (DPAN).
applicationExpirationDateapplication_expiration_dateCóYYMMDD, chính xác như Apple phát ra. Không định dạng lại thành MM/YY.
currencyCodecurrency_codeCóISO 4217 dạng số, dưới dạng chuỗi để giữ được số 0 ở đầu.
transactionAmounttransaction_amountCóApple phát ra một số JSON; Bahn nhận một chuỗi thập phân. Chuyển thành chuỗi, không rescale nó.
cardholderNamecardholder_nameKhôngĐược chấp nhận để tương thích với token của Apple; không được lưu trữ.
deviceManufacturerIdentifierdevice_manufacturer_identifierCóChuỗi hex.
paymentData.onlinePaymentCryptogrampayment_data.online_payment_cryptogramCóTAVV dạng base64. Đây là thứ khiến DPAN có thể tính phí.
paymentData.eciIndicatorpayment_data.eci_indicatorKhôngMột số token bỏ qua trường này. Nếu có, giữ nguyên khi truyền qua.
header.transactionId (từ envelope, không phải từ plaintext)transaction_idCóCơ chế chống replay của Bahn — xem bên dưới.

WARNING

Chỉ paymentDataType: "3DSecure" được chấp nhận. Token EMV có cấu trúc paymentData khác, không thể biểu diễn trong decrypted_token, và sẽ bị từ chối. Nếu bạn thấy EMV, PKPaymentRequest.merchantCapabilities của bạn đang thiếu .capability3DS.

INFO

transaction_id được giới hạn theo từng merchant và chỉ dùng được một lần: một giao dịch Apple Pay nhất định chỉ có thể tạo ra một thẻ đúng một lần. Một lần confirm lặp lại với cùng transaction_id sẽ bị từ chối, vì vậy hãy luôn kèm Bn-Idempotency-Key cho mỗi lần confirm thay vì dựa vào việc thử lại lần hai thành công.


Xác nhận với token đã giải mã ​

Tạo PaymentIntent giống hệt như trong luồng cơ bản — không có gì đặc thù cho Apple ở giai đoạn này. Lệnh gọi confirm là nơi hai biến thể khác nhau.

http
POST /api/payment_intents/{id}/confirm

Request

json
{
  "payment_method_data": {
    "apple_pay": {
      "decrypted_token": {
        "application_primary_account_number": "4111111111111111",
        "application_expiration_date": "281231",
        "currency_code": "704",
        "transaction_amount": "8000",
        "cardholder_name": "Nguyen Van A",
        "device_manufacturer_identifier": "040010030273",
        "payment_data": {
          "online_payment_cryptogram": "AoWXNg6NkAAAAAAAAAAAAAA=",
          "eci_indicator": "05"
        },
        "transaction_id": "31323334353637383930"
      }
    }
  }
}
TrườngBắt buộcMô tả
payment_method_data.apple_pay.decrypted_tokenCóToken Apple Pay đã giải mã, ánh xạ như trong Ánh xạ dữ liệu đã giải mã. Loại trừ lẫn nhau với card, napas_qr, card_present, và google_pay.
payment_method_optionsKhôngCác tùy chọn riêng cho từng lần xác nhận. Bỏ qua trừ khi Bahn đã thông báo khác cho tài khoản của bạn.
capture_methodKhôngĐặt thành manual để ủy quyền ngay và capture sau. Xem Manual capture.

Cùng cấu trúc này cũng hoạt động trên POST /api/payment_intents với confirm: true, nếu bạn muốn tạo và xác nhận trong cùng một lệnh gọi.

Xác thực lệnh gọi này như bất kỳ request server-to-server nào khác: Bn-Api-Key cùng với một body đã ký JWS. Xem Thực hiện request đầu tiên.

bash
curl -X POST 'https://bn.cakedigibank.tech/api/payment_intents/pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B/confirm' \
  -H 'Content-Type: application/jose' \
  -H 'Bn-Api-Key: api_key-ab318df034' \
  -H 'Bn-Jose-Method: jws' \
  -H 'Bn-Request-Id: 7f3e9c1a-4b2d-4e8f-a1c0-123456789abc' \
  -H 'Bn-Idempotency-Key: order-1234-applepay' \
  -d '<compact JWS of the JSON body above>'

Response

json
{
  "payment_intent": {
    "id": "pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B",
    "amount": 8000,
    "currency": "704",
    "status": "succeeded",
    "payment_method_id": "pm_01HZ3XA2K4T7QJ0M9C5N8E1D3F",
    "latest_charge_id": "ch_01HZ3XA2K4T7QJ0M9C5N8E1D3G"
  }
}

PaymentMethod kết quả là một thẻ đã được device-tokenize gắn nhãn ví điện tử, giống hệt những gì luồng cơ bản tạo ra:

json
{
  "type": "card",
  "card": {
    "brand": "visa",
    "last4": "1234",
    "wallet": { "type": "apple_pay" },
    "network_token": {
      "dynamic_last4": "4242",
      "token_requestor_id": "40010030273"
    }
  }
}

INFO

Luôn đặt Bn-Idempotency-Key cho lệnh gọi này, gắn theo order của bạn. Nếu app retry vì response bị mất, idempotency sẽ phát lại response gốc — transaction_id chỉ dùng được một lần, nên nếu không có idempotency, một lần thử lại thực sự sẽ thất bại một cách khó hiểu.

Xử lý webhook giống hệt như trong luồng cơ bản: payment_intent.status.succeeded là nguồn sự thật (source of truth), không phải dấu tick trên PassKit. Xem Apple Pay → Xử lý webhook.

Xử lý lỗi ​

Đây là các lỗi đặc thù cho việc tự giải mã. Với các lỗi chung (nút không bao giờ xuất hiện, sheet bị timeout), xem trang cơ bản.

Triệu chứngNguyên nhân khả dĩCách khắc phục
Việc giải mã của bạn thất bại hoặc cho ra dữ liệu vô nghĩaToken được mã hóa bằng một Payment Processing key khácChọn key theo header.publicKeyHash, không theo thứ tự cấu hình. Trong quá trình xoay vòng chứng chỉ, cả hai key phải luôn có thể tải được.
Xác minh chữ ký thất bạipaymentData bị bọc base64, serialize lại, hoặc bị cắt xén giữa app và backend của bạnChuyển tiếp đúng chuỗi JSON UTF-8 thô như PassKit đã tạo ra.
Confirm bị từ chối, plaintext trông có vẻ đúngapplication_expiration_date bị định dạng lại thành MM/YY, hoặc transaction_amount được gửi dưới dạng số JSONGiữ nguyên định dạng YYMMDD của Apple, và chuyển số tiền thành chuỗi mà không rescale.
Confirm bị từ chối và đề cập đến loại dữ liệu thanh toánToken là EMV, không phải 3DSecureThêm .capability3DS vào PKPaymentRequest.merchantCapabilities.
Confirm bị từ chối với lỗi thiếu trườngtransaction_id được đọc từ payload đã giải mã thay vì từ header của envelopeNó nằm ở paymentData.header.transactionId, bên ngoài phần ciphertext.
Confirm thất bại khi retrytransaction_id chỉ dùng được một lần cho mỗi merchantGửi cùng Bn-Idempotency-Key để response gốc được phát lại thay vì một lần ủy quyền mới được thử.

Kiểm thử ​

Sử dụng một thiết bị vật lý đã đăng nhập vào một Apple ID sandbox với một thẻ test Apple Pay Sandbox trong Wallet, trỏ đến host sandbox của Bahn https://bn.cakedigibank.tech. Apple Pay không thể được kiểm thử trên iOS Simulator với thẻ thật.

Token sandbox được mã hóa bằng cùng chứng chỉ như token production, vì vậy luồng giải mã của bạn được kiểm thử end-to-end đầy đủ. Một điểm kiểm tra ban đầu hữu ích là xác nhận paymentDataType đã giải mã là 3DSecure trước khi gọi Bahn — điều này giúp phân biệt "mã hóa của tôi sai" với "ánh xạ dữ liệu của tôi sai".

Developers