SKAdNetwork S2S 工作流程:如何驗證廣告聯播網 Postback

opoinstall
2026-08-21
5 min read

DSP 如何處理 SKAdNetwork postback?需求方平台(DSP)與廣告聯播網透過建立安全的 HTTP POST 接收端點、使用 U+2063 分隔符號建構序列化的 UTF-8 訊息字串、針對 Apple 公布的公開金鑰驗證 Apple 的密碼學 ECDSA P-256 簽章,並記錄已驗證的交易 ID 以防止重複處理,然後再更新出價模型。

SKAdNetwork 安裝驗證 postback 是一則經 Apple 簽署的 HTTPS POST 通知,作業系統會將其傳送至符合資格的廣告聯播網;若為贏得歸因的流量,則會選擇性地傳送至所宣傳應用程式開發人員所設定的複製端點。為了確保資料完整性,後端接收系統必須驗證 Apple 的 ECDSA P-256 簽章、驗證參數序列化,並強制執行交易層級的去重複機制。

術語 定義
SKAdNetwork Apple 用於維護隱私的廣告活動歸因平台級框架。
安裝驗證 Postback 在發生符合資格的廣告轉換後,包含安裝驗證與歸因後設資料的 Apple 簽署 JSON 酬載。
ECDSA P-256 Apple 用於簽署安裝驗證 postback 的橢圓曲線密碼學演算法。
Transaction ID 接收端用作重複偵測冪等性金鑰的唯一驗證識別碼。

DSP 與廣告聯播網的 SKAdNetwork Postback 接收架構

雙軌接收管線:直接廣告聯播網投遞與開發人員 Postback 端點

當發生已歸因的 iOS 應用程式安裝時,Apple 的歸因子系統會透過 HTTPS POST 發送安裝驗證 postback:

  • 廣告聯播網接收:裝置會直接將主要的贏得 postback(did-win: true)傳送至 Apple 註冊表中對應 ad-network-id 下所註冊的伺服器 URL。
  • 開發人員複本接收:如果受宣傳的應用程式在其 Info.plist 中指定了 NSAdvertisingAttributionReportEndpoint 金鑰,裝置會同時將贏得 postback 的精確複本直接發送至開發人員的伺服器。
  • 未贏得 Postback 路由:自 SKAdNetwork 3.0 開始,若有多個廣告聯播網符合歸因條件但未贏得歸因,裝置會將最多五個未贏得的 postback(did-win: false)直接傳送給那些次要符合條件的廣告聯播網。未贏得的 postback 不會傳送至開發人員複本端點。

後端接收端點應回應 HTTP 200 OK。若裝置未收到 200 回應,可能會在最多九天內重試發送最多九次。

SKAdNetwork postback ingestion and verification workflow

NSAdvertisingAttributionReportEndpoint 在開發人員稽核中的角色

NSAdvertisingAttributionReportEndpoint 讓應用程式開發人員能夠接收贏得 postbacks 的直接複本,而無需依賴廣告聯播網轉發:

  • 獨立稽核:開發人員會收到為其應用程式產生的所有贏得 postbacks 的精確複本,從而能夠內部驗證廣告聯播網的報表。
  • 專用端點路徑:開發人員伺服器必須將端點代管於 https://<domain>/.well-known/skadnetwork/report-attribution/
  • AdAttributionKit 區別:對於 AdAttributionKit,Apple 定義了獨立的組態路由至 https://<domain>/.well-known/appattribution/report-attribution/,其採用 JSON 網路簽章(JWS)驗證架構。

MMP 如何接收、聚合與標準化多網路 S2S 事件串流

根據商業整合方式,行動衡量夥伴(MMP)可能會透過開發人員端轉發、廣告聯播網整合或自訂夥伴伺服器流程來接收 SKAdNetwork 資料:

  • 多來源接收:接收從開發人員端點轉發的已驗證 postback 資料,以及直接的廣告聯播網報表串流。
  • 跨串流去重複:在共用的廣告聯播網與開發人員複本之間,使用唯一的 transaction-id 來標準化並去除重複記錄。
  • 下游 BI 標準化: 將粗粒度與細粒度的轉換值對應至客戶定義的收益模型與漏斗事件。

延伸閱讀:SKAdNetwork ──> 行動歸因模型

密碼學驗證:驗證 Apple 的 ECDSA P-256 簽章

理解密碼學堆疊:搭配 SHA-256 的 NIST 曲線 P-256 (secp256r1)

每個 SKAdNetwork postback 都包含一個 attribution-signature 欄位。此密碼學簽章是由 Apple 使用橢圓曲線數位簽章演算法(ECDSA)搭配 NIST P-256(secp256r1)曲線與 SHA-256 摘要所產生。

該簽章驗證了兩個基本的安全性屬性:

  • 真實性:postback 是由 Apple 的平台子系統直接在已驗證的裝置上產生,而非由惡意的用戶端或代理伺服器偽造。
  • 完整性:簽章所涵蓋的參數在傳輸過程中未遭到竄改。

使用 Apple 公布的 SKAdNetwork 公開金鑰

為了驗證簽章,接收伺服器必須載入 Apple 的官方公開金鑰。對於 SKAdNetwork 2.1 及更新版本,Apple 在其開發人員文件中公布了專用的 NIST P-256 公開金鑰:

  • 金鑰初始化:在伺服器初始化期間,公開金鑰會以標準 X.509/DER 公開金鑰物件形式載入記憶體中。
  • 非對稱簽章檢查:驗證引擎會重建確切的 UTF-8 序列化訊息字串、計算 SHA-256 雜湊,並針對重建的訊息驗證經 Base64 解碼的 attribution-signature

[Device / Subsystem] ──► [Dispatches Signed JSON Postback]
                                     │
                                     ▼
                  [DSP / Ad Network Ingestion Endpoint]
                  (HTTPS POST to registered postback URL)
                                     │
                                     ▼
                  [Parse JSON & Reconstruct Message String]
                  (Concatenate UTF-8 fields with \u2063)
                                     │
                                     ▼
                  [ECDSA P-256 Public Key Signature Verification]
                                     │
                      ┌──────────────┴──────────────┐
                      ▼                                                          ▼
               [Signature Valid]                                         [Signature Invalid]
                      │                                                          │
                      ▼                                                          ▼
            [Atomic Deduplication]                                       [Log Error & Discard]
            (Check transaction-id)
                      │
                      ▼
            [Process Attribution]
SKAdNetwork ECDSA P 256 signature verification flow

為何單靠雜湊並不足夠:非對稱簽章驗證

由於 Apple 是使用其私密金鑰對酬載進行簽署,且不會分發共用密鑰,因此無法使用對稱驗證(例如 HMAC-SHA256)。接收引擎必須使用標準的密碼學函式庫(例如 OpenSSL、Node.js crypto 或 Python cryptography)來實作標準的非對稱公開金鑰簽章驗證。

建構用於簽章驗證的訊息字串

嚴格的序列化協定:隱形分隔符號(\u2063)的角色

Apple 指定了確切的 UTF-8 位元組序列化格式來建構用於簽章驗證的訊息字串。參數必須以精確的順序串接,並以隱形的 Unicode 字元 \u2063(U+2063 隱形分隔符號,UTF-8 位元組序列 0xE2 0x81 0xA3)隔開:

Message=Param1    "2˘063"    Param2    "2˘063"  Paramn\text{Message} = \text{Param}_1 \;\|\; \text{"\u2063"} \;\|\; \text{Param}_2 \;\|\; \text{"\u2063"} \dots \|\; \text{Param}_n

替換空白、標準標點符號或替代的 Unicode 分隔符號將導致密碼學驗證失敗。

SKAN 4.0 的版本專屬參數順序

根據 Apple 開發人員關於驗證安裝驗證 Postback 的文件,SKAdNetwork 4.0 postbacks 的參數必須按照以下精確順序進行序列化:

  1. version(例如 "4.0"
  2. ad-network-id(例如 "example123.skadnetwork"
  3. source-identifier(例如 "4821"
  4. app-id(例如 1234567890
  5. transaction-id(例如 "6a8b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d"
  6. redownload(例如作為小寫字串的 "true""false"
  7. source-app-id(針對應用程式對應用程式廣告) source-domain(針對 Safari 中的網頁對應用程式廣告),僅在 postback 中存在時包含
  8. fidelity-type(例如 StoreKit 渲染的廣告或 SKAdNetwork 歸因網頁廣告為 1;瀏覽歸因廣告為 0
  9. did-win(例如作為小寫字串的 "true""false"
  10. postback-sequence-index(例如 012

關鍵的 SKAN 4 規格:轉換值不包含在簽章中

在 SKAdNetwork 4.0 中,即使 JSON 酬載中存在其中一個欄位,Apple 的簽章也不包含 conversion-valuecoarse-conversion-value。SKAN 4 的序列化字串結尾為 postback-sequence-index。嘗試將轉換值附加到訊息字串將導致驗證失敗。

底下的 JSON 酬載說明了完整的 SKAdNetwork 4.0 postback 結構描述。下方的簽章為示意性佔位符,無法通過密碼學驗證;進行單元測試時,請使用官方驗證文件中的 Apple 已簽署範例:

{
  "version": "4.0",
  "ad-network-id": "example123.skadnetwork",
  "source-identifier": "4821",
  "app-id": 1234567890,
  "transaction-id": "6a8b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d",
  "redownload": false,
  "source-app-id": 9876543210,
  "fidelity-type": 1,
  "did-win": true,
  "postback-sequence-index": 0,
  "conversion-value": 47,
  "attribution-signature": "MEQCIFz8...SAMPLE_CRYPTOGRAPHIC_SIGNATURE...=="
}

SKAN 4 signature message serialization with U 2063

處理多視窗 SKAN 4.0 酬載與開發人員端點

跨連續轉換視窗解析 postback-sequence-index

在 SKAdNetwork 4.0 中,轉換會產生橫跨首次應用程式啟動後最多 35 天的轉換視窗的 postbacks,實際交付則會在 Apple 隨機化的視窗後延遲發生。後端接收系統會解析 postback-sequence-index 欄位,以將轉換資料指派至正確的生命週期視窗:

  • 索引 0(視窗 1:第 0–2 天):包含細粒度轉換值(0–63)、粗粒度轉換值(lowmediumhigh),或該欄位不存在。
  • 索引 1(視窗 2:第 3–7 天):針對 postback 資料層級 1–3,在提供時可能會揭露 coarse-conversion-valuelowmediumhigh);層級 0 不符合第二或第三次 postback 的資格。
  • 索引 2(視窗 3:第 8–35 天):針對 postback 資料層級 1–3,在提供時可能會揭露 coarse-conversion-valuelowmediumhigh);層級 0 不符合第二或第三次 postback 的資格。

管理細粒度與粗粒度轉換值

接收解碼器必須考量酬載的可變性:

  • 互斥性:Apple 規定安裝驗證 postback 可以包含 conversion-valuecoarse-conversion-value 其中之一,但絕不能同時包含兩者。
  • 無轉換值:如果指定的 postback 資料層級較低(層級 0),則會從 JSON 酬載中省略轉換值欄位。

防範重送攻擊與偽造轉換酬載

transaction-id 作為去重複金鑰的角色

每個 SKAdNetwork postback 都包含一個唯一的 transaction-id UUID。Apple 文件建議接收者將此識別碼用作冪等性金鑰,以偵測並捨棄重複的轉換 postbacks。

由於 postback 傾聽程式是公開可及的 HTTPS 端點,惡意攻擊者可能會嘗試透過擷取有效的 postback 並重複提交來人工灌水轉換指標。

實作分散式記憶體快取與持續性分類帳

Apple 並未規定通用的去重複保留期限。生產環境接收者應根據其對帳與重送防禦需求,為已驗證的交易 ID 維持持久的冪等性記錄;Redis TTL 可用作熱快取優化,而非唯一的權威重複分類帳:

  1. 優先進行密碼學驗證:在將交易 ID 提交至儲存空間之前,針對 Apple 的公開金鑰完整驗證 ECDSA 簽章。
  2. 原子去重複:執行由持久性關聯式或文件資料庫唯一條件約束所支援的原子寫入操作(例如 Redis SET key value NX EX <seconds>)。
  3. 去重複範圍:在快取層中設定一個營運保留視窗,涵蓋預期的 postback 交付、網路重試(Apple 會對失敗的交付重試長達 9 天)以及下游對帳。

SKAdNetwork replay defense and deduplication pipeline


底下的後端實作示範了 Python 中的簽章驗證、結構描述驗證與原子去重複:

import base64
import json
import redis
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.exceptions import InvalidSignature

# Initialize Redis client for hot-cache transaction deduplication
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# Official Apple SKAdNetwork 2.1+ Public Key (Base64 DER encoded, published by Apple)
APPLE_SKAN_PUBLIC_KEY_B64 = (
    "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWdp8GPcGqmhgzEFj9Z2nSpQVdday"
    "aPe4FMzqM9wib1+aHaaIzoHoLN9zW4K8y4SPykE3YVK3sVqW6Af0lfx3gg=="
)

# Exact Apple-specified invisible separator: U+2063 INVISIBLE SEPARATOR (UTF-8: 0xE2 0x81 0xA3)
SEPARATOR = "\u2063"

def construct_skan4_message_bytes(payload: dict) -> bytes:
    """
    Constructs the serialized UTF-8 message string for SKAN 4.0 signature verification.
    Apple specification explicitly EXCLUDES conversion-value and coarse-conversion-value from the signature.
    """
    parts = [
        str(payload["version"]),
        str(payload["ad-network-id"]),
        str(payload["source-identifier"]),
        str(payload["app-id"]),
        str(payload["transaction-id"]),
        "true" if payload["redownload"] is True else "false"
    ]

    # Include source-app-id (app ad) OR source-domain (web ad) if present
    if payload.get("source-app-id") is not None:
        parts.append(str(payload["source-app-id"]))
    elif payload.get("source-domain") is not None:
        parts.append(str(payload["source-domain"]))

    parts.append(str(payload["fidelity-type"]))
    parts.append("true" if payload["did-win"] is True else "false")
    parts.append(str(payload["postback-sequence-index"]))

    # Join with U+2063 separator and encode to UTF-8
    message_string = SEPARATOR.join(parts)
    return message_string.encode('utf-8')

def verify_and_ingest_skan4_postback(postback_json_str: str) -> dict:
    """
    Validates payload schema, verifies the ECDSA P-256 signature against Apple's public key,
    and performs atomic transaction-id deduplication.
    """
    try:
        payload = json.loads(postback_json_str)
    except Exception:
        return {"status": "REJECTED", "reason": "INVALID_JSON_FORMAT"}

    # Version Gate: Enforce SKAN 4.0 payload handling
    if payload.get("version") != "4.0":
        return {"status": "REJECTED", "reason": "UNSUPPORTED_SKAN_VERSION"}

    # Schema Validation: Required fields for SKAN 4.0
    required_fields = [
        "version", "ad-network-id", "source-identifier", "app-id",
        "transaction-id", "redownload", "fidelity-type", "did-win",
        "postback-sequence-index", "attribution-signature"
    ]
    for field in required_fields:
        if field not in payload:
            return {"status": "REJECTED", "reason": f"MISSING_REQUIRED_FIELD_{field.upper()}"}

    # Strict Type Validation
    if not isinstance(payload["redownload"], bool):
        return {"status": "REJECTED", "reason": "INVALID_TYPE_REDOWNLOAD"}
    if not isinstance(payload["did-win"], bool):
        return {"status": "REJECTED", "reason": "INVALID_TYPE_DID_WIN"}
    if payload["postback-sequence-index"] not in (0, 1, 2):
        return {"status": "REJECTED", "reason": "INVALID_SEQUENCE_INDEX"}
    if payload["fidelity-type"] not in (0, 1):
        return {"status": "REJECTED", "reason": "INVALID_FIDELITY_TYPE"}

    # Enforce mutual exclusivity between source-app-id and source-domain
    has_source_app = payload.get("source-app-id") is not None
    has_source_domain = payload.get("source-domain") is not None
    if has_source_app and has_source_domain:
        return {"status": "REJECTED", "reason": "CONFLICTING_SOURCE_FIELDS"}

    signature_b64 = payload["attribution-signature"]
    transaction_id = payload["transaction-id"]

    try:
        # Step 1: Reconstruct the exact UTF-8 serialized message
        message_bytes = construct_skan4_message_bytes(payload)
        signature_der = base64.b64decode(signature_b64, validate=True)

        # Step 2: Verify ECDSA P-256 / SHA-256 signature using Apple's published public key
        apple_public_key = load_der_public_key(base64.b64decode(APPLE_SKAN_PUBLIC_KEY_B64))
        apple_public_key.verify(
            signature_der,
            message_bytes,
            ec.ECDSA(hashes.SHA256())
        )
    except InvalidSignature:
        return {"status": "REJECTED", "reason": "INVALID_CRYPTOGRAPHIC_SIGNATURE"}
    except Exception as e:
        return {"status": "ERROR", "reason": f"VERIFICATION_FAILED: {str(e)}"}

    # Step 3: Atomic Deduplication via Redis (Hot cache layer)
    # Note: In production, pair this hot cache with a persistent unique database constraint.
    # 14-day TTL (1,209,600 seconds) serves as an illustrative receiver cache policy covering retries.
    is_new = redis_client.set(f"skan_tx:{transaction_id}", "1", nx=True, ex=1209600)
    if not is_new:
        return {"status": "DUPLICATE", "reason": "TRANSACTION_ALREADY_PROCESSED"}

    return {
        "status": "VERIFIED",
        "transaction_id": transaction_id,
        "sequence_index": payload["postback-sequence-index"],
        "did_win": payload["did-win"]
    }

將 Postbacks 融入即時出價模型與 CPA 優化器

將邊緣接收與非同步處理進行解耦

高容量的 DSP 在廣告活動高峰期間會處理大量的 postbacks。同步的下游處理可能會引入延遲瓶頸。

企業架構會實作非同步管線:

  1. 邊緣接收器:接受進來的 HTTP POST、驗證簽章真實性、在 transaction-id 上執行原子去重複,並立即傳回 HTTP 200 OK
  2. 事件佇列:將已驗證的酬載發布至分散式事件仲介機構(例如 Apache Kafka 或 AWS SQS)。
  3. 出價與分析工作執行緒:消耗事件串流、將轉換值對應至收益指標,並更新即時出價(RTB)目標 CPA 模型。

善用階層式來源識別碼

根據 postback 資料層級,第一個贏得的 postback 可能會暴露階層式 source-identifier 的兩位數、三位數或四位數。這些數字的語義由廣告聯播網本身的來源識別碼分類法所定義。出價系統應根據聯播網本身的廣告活動後設資料來解析接收到的 source-identifier,而不是假設位數長度與特定版位或素材粒度之間存在通用的對應關係。

比較矩陣:Apple 直接投遞與 MMP S2S 接收

功能面向 Apple 直接投遞(廣告聯播網) 開發人員端點(NSAdvertising... MMP S2S 接收管線
接收者 已註冊的廣告聯播網 受宣傳的應用程式開發人員 行動衡量夥伴
歸因範圍 該聯播網的贏得 Postbacks 應用程式贏得 Postbacks 的複本 多網路聚合檢視
簽章驗證 由廣告聯播網後端執行 由開發人員後端執行 視實作而定(夥伴流程)
未贏得 Postbacks 符合資格即會收到(did-win: false 不會傳送至開發人員端點 可能透過夥伴流程提供
主要使用案例 直接出價者與目標 CPA 優化 內部倉儲稽核與驗證 跨通路成效儀表板

常見問題(FAQ)

用來驗證 Apple postback 簽章的是哪個公開金鑰?
Apple 在其開發人員文件中公布了用於 SKAdNetwork 2.1+ 安裝驗證的官方 NIST P-256 公開金鑰。接收伺服器會以 X.509/DER 格式載入此公開金鑰,以驗證進來的簽章。
為什麼有效的 SKAdNetwork postback 會未通過簽章驗證?
簽章驗證失敗通常是由序列化錯誤所引起:使用錯誤的分隔符號(使用 `\u2060` 而非 `\u2063`)、參數順序不正確、錯誤地將轉換值附加到 SKAN 4 訊息字串中,或是錯誤處理布林值字串編碼(`"true"` 對 `"false"`)。
受宣傳應用程式的開發人員端點可以接收未贏得的 SKAdNetwork postbacks 嗎?
不行。設定後,開發人員複本端點會接收贏得的安裝驗證 postbacks 的複本。最多五個未贏得的 postbacks(<code>did-win: false</code>)會直接傳送給其他符合資格的廣告聯播網,而不會傳送至開發人員複本端點。

總結與決策框架

在大規模下處理 SKAdNetwork postbacks 需要結合低延遲邊緣接收與嚴謹的密碼學驗證以及交易層級去重複。由於 Apple postbacks 直接影響預算配置與出價演算法,驗證 ECDSA 簽章並強制執行 transaction-id 冪等性可保護接收管線免受偽造或竄改的 postbacks 以及重複重送處理的影響。

為了以微觀層級的使用者引導與即時深度連結路由來互補平台中介的 SKAdNetwork 報表,工程團隊會在平台 API 之外部署第一方路由架構。

若要進一步了解如何設定伺服器端歸因 postbacks 與深度連結管線,請參閱 OpoInstall 文件

相關資料

  • 概念:S2S Postbacks、密碼學驗證、ECDSA P-256、重送攻擊防禦、交易去重複

  • 技術:Apple SKAdNetwork、Apple AdAttributionKit、Redis 記憶體快取、OpoInstall Mobile SDK

  • 標準:IETF RFC 8259(JSON 資料交換)、RFC 5480(橢圓曲線密碼學)

  • API:StoreKit SKAdNetwork API、Apple S2S Postback 交付規格、OpoInstall S2S API

官方文件

Share this article