SKAdNetwork S2S ワークフロー:広告ネットワークのポストバックを検証する方法

opoinstall
2026-08-21
5 min read

DSPはSKAdNetworkのポストバックをどのように処理するのでしょうか? デマンドサイドプラットフォーム(DSP)および広告ネットワークは、安全なHTTPS POST取り込みエンドポイントを構築し、U+2063区切り文字を用いたシリアライズ済みUTF-8メッセージ文字列の生成、Appleが公開する公開鍵に対するAppleの暗号学的ECDSA P-256署名の検証、および入札モデルを更新する前の二重処理を防ぐための検証済みトランザクションIDの記録を行うことで、SKAdNetworkのポストバックを処理します。

SKAdNetworkのインストール検証ポストバックとは、オペレーティングシステムが適格な広告ネットワーク(および獲得アトリビューションの場合はオプションで、宣伝対象アプリのデベロッパーが設定したコピー先エンドポイント)へ送信する、Apple署名入りのHTTPS POST通知のことです。データの整合性を確保するため、バックエンドの取り込みシステムは、AppleのECDSA P-256署名の検証、パラメータのシリアライズ検証、およびトランザクションレベルの重複排除を確実に実行する必要があります。

用語 定義
SKAdNetwork プライバシーを保護しながら広告キャンペーンの効果測定を行うための、Appleのプラットフォームレベルのフレームワーク。
インストール検証ポストバック 適格な広告コンバージョン後に送信される、インストール検証およびアトリビューションのメタデータを含むApple署名済みのJSONペイロード。
ECDSA P-256 インストール検証ポストバックの署名にAppleが使用する楕円曲線暗号アルゴリズム。
トランザクション ID 受信側が重複検出のためのべき等キーとして使用する、一意の検証識別子。

DSPおよび広告ネットワークにおけるSKAdNetworkポストバック取り込みのアーキテクチャ

二重取り込みパイプライン:広告ネットワークへの直接配信対デベロッパー用ポストバックエンドポイント

アトリビューションが成立したiOSアプリのインストールが発生すると、AppleのアトリビューションサブシステムはHTTPS POST経由でインストール検証ポストバックをディスパッチします:

  • 広告ネットワークへの取り込み: デバイスは、Appleのレジストリ内の対応する ad-network-id に登録されたサーバーURLへ、プライマリの獲得ポストバック(did-win: true)を直接配信します。
  • デベロッパー用コピーの取り込み: 宣伝対象アプリの Info.plistNSAdvertisingAttributionReportEndpoint キーが指定されている場合、デバイスは獲得ポストバックの正確なコピーをデベロッパーのサーバーへも同時に直接ディスパッチします。
  • 非獲得ポストバックのルーティング: SKAdNetwork 3.0以降では、アトリビューションの条件を満たしながらも獲得に至らなかった広告ネットワークが複数存在する場合、デバイスはそれらの二次的な適格広告ネットワークに対して、最大5つの非獲得ポストバック(did-win: false)を直接送信します。なお、非獲得ポストバックはデベロッパー用のコピーエンドポイントには配信されません。

バックエンドの取り込みエンドポイントは、HTTP 200 OK で応答する必要があります。デバイスが 200 の応答を受信しない場合、最長9日間にわたり最大9回の配信リトライが行われることがあります。

SKAdNetwork postback ingestion and verification workflow

デベロッパーによる監査における NSAdvertisingAttributionReportEndpoint の役割

NSAdvertisingAttributionReportEndpoint を使用すると、アプリデベロッパーは広告ネットワークの転送とは独立して、獲得ポストバックの直接のコピーを受け取ることができます:

  • 独立した監査: デベロッパーは自身のアプリ向けに生成されたすべての獲得ポストバックの正確なコピーを受け取ることができ、広告ネットワークからのレポートの内部検証が可能になります。
  • 専用のエンドポイントパス: デベロッパーサーバーは、https://<domain>/.well-known/skadnetwork/report-attribution/ にエンドポイントをホストする必要があります。
  • AdAttributionKitとの違い: AdAttributionKitの場合、Appleは https://<domain>/.well-known/appattribution/report-attribution/ へのルーティング設定を別途定義しており、こちらではJSON Web Signature(JWS)検証アーキテクチャが使用されます。

MMPによるマルチネットワークS2Sイベントストリームの取り込み、集約、および正規化

商用連携の形態に応じて、モバイル計測パートナー(MMP)は、デベロッパー側の転送、広告ネットワークとの連携、またはカスタムパートナーサーバーのフローを介してSKAdNetworkデータを取り込む場合があります:

  • マルチソースの取り込み: 広告ネットワークからの直接レポートに加え、デベロッパーのエンドポイントから転送された検証済みポストバックデータを取り込みます。
  • ストリーム間の重複排除: 共有される広告ネットワークのコピーとデベロッパーのコピーの間で、一意の transaction-id を使用してレコードを正規化および重複排除します。
  • 下流BIシステムの正規化: 粗粒度および細粒度のコンバージョン値を、クライアント定義の収益モデルやファネルイベントにマッピングします。

関連情報:SKAdNetwork ──> モバイルアトリビューションモデル

暗号学的検証:AppleのECDSA P-256署名の検証

暗号スタックの理解:NIST曲線 P-256(secp256r1)とSHA-256

すべてのSKAdNetworkポストバックには attribution-signature フィールドが含まれています。この暗号署名は、NIST P-256(secp256r1)曲線とSHA-256ダイジェストを用いた楕円曲線デジタル署名アルゴリズム(ECDSA)を使用してAppleが生成したものです。

この署名により、以下の2つの基本的なセキュリティ特性が検証されます:

  • 正当性: ポストバックが悪意あるクライアントやプロキシによって偽造されたものではなく、検証済みデバイス上の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デベロッパー向けドキュメントによると、SKAdNetwork 4.0のポストバックのパラメータは、以下の正確な順序でシリアライズする必要があります:

  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でのWebtoApp広告の場合。ポストバックに存在する場合のみ含める)
  8. fidelity-type(例:StoreKitレンダリング広告またはSKAdNetworkアトリビューション対象のWeb広告の場合は 1、インプレッション計測広告の場合は 0
  9. did-win(例:小文字の文字列としての "true" または "false"
  10. postback-sequence-index(例:01、または 2

SKAN 4仕様の重要事項:コンバージョン値は署名から除外される

SKAdNetwork 4.0では、JSONペイロードのいずれかのフィールドに存在する場合であっても、Appleの署名に conversion-valuecoarse-conversion-value は含まれません。SKAN 4のシリアライズ済み文字列は postback-sequence-index で終了します。メッセージ文字列にコンバージョン値を追加しようとすると、検証が失敗する原因となります。

以下のJSONペイロードは、完全なSKAdNetwork 4.0ポストバックのスキーマを示しています。以下の署名はサンプル用のプレースホルダーであり、暗号学的検証には合格しません。単体テストを行う際は、公式検証ドキュメントにある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日間に及ぶコンバージョンウィンドウからコンバージョンによりポストバックが生成されます。実際の配信は、Appleによるランダム化されたウィンドウ後の遅延を経て行われます。バックエンドの取り込みシステムは、postback-sequence-index フィールドを解析して、コンバージョンデータを正しいライフサイクルウィンドウに割り当てます:

  • インデックス 0(ウィンドウ1:0〜2日目):高精度コンバージョン値(0〜63)、粗粒度コンバージョン値(lowmediumhigh)のいずれか、またはフィールド自体が含まれません。
  • インデックス 1(ウィンドウ2:3〜7日目):ポストバックデータティア1〜3の場合、提供されていれば coarse-conversion-valuelowmediumhigh)が公開されることがあります。ティア0は2回目または3回目のポストバックの対象外です。
  • インデックス 2(ウィンドウ3:8〜35日目):ポストバックデータティア1〜3の場合、提供されていれば coarse-conversion-valuelowmediumhigh)が公開されることがあります。ティア0は2回目または3回目のポストバックの対象外です。

高精度コンバージョン値と粗粒度コンバージョン値の管理

取り込みデコーダーは、ペイロードの変動に対応する必要があります:

  • 排他性:インストール検証ポストバックには、conversion-value または coarse-conversion-value のいずれか一方を含めることはできますが、両方を同時に含めることはできないとAppleは規定しています。
  • コンバージョン値がない場合:割り当てられたポストバックデータティアが低い(ティア0)場合、コンバージョン値のフィールドはJSONペイロードから省略されます。

リプレイ攻撃およびなりすましコンバージョンペイロードからの防御

重複排除キーとしての transaction-id の役割

すべてのSKAdNetworkポストバックには、一意の transaction-id UUIDが含まれています。Appleのドキュメントでは、重複したコンバージョンポストバックを検出して破棄するためのべき等キーとして、この識別子を使用することを推奨しています。

ポストバックリスナーは外部からアクセス可能なHTTPSエンドポイントであるため、悪意ある攻撃者が有効なポストバックをキャプチャし、コンバージョン指標を人工的に水増しするために何度も再送信するリプレイ攻撃を試みる可能性があります。

分散インメモリキャッシュと永続的台帳の実装

Appleは、普遍的な重複排除の保持期間を指定していません。本番環境のレシーバーは、照合やリプレイ防御の要件に応じて、検証済みのトランザクションIDに対する耐久性のあるべき等レコードを維持する必要があります。RedisのTTLは、唯一の信頼できる重複台帳としてではなく、ホットキャッシュの最適化として使用できます:

  1. 暗号学的検証の優先:トランザクションIDをストレージにコミットする前に、Appleの公開鍵に対してECDSA署名を完全に検証します。
  2. アトミックな重複排除:永続的なリレーショナルデータベースまたはドキュメントデータベースの一意性制約に裏付けられたアトミックな書き込み操作(例:Redisの SET key value NX EX <seconds>)を実行します。
  3. 重複排除の期間:想定されるポストバック配信、ネットワークのリトライ(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"]
    }

リアルタイム入札モデルおよびCPAオプティマイザーへのポストバックの取り込み

エッジ取り込みと非同期処理の分離

大容量のDSPは、キャンペーンのピーク期間中に膨大な量のポストバックを処理します。同期型の下流処理では、レイテンシのボトルネックが発生する可能性があります。

エンタープライズアーキテクチャでは、非同期パイプラインを実装しています:

  1. エッジレシーバー:着信したHTTP POSTを受け入れ、署名の正当性を検証し、transaction-id に対してアトミックな重複排除を実行し、すぐにHTTP 200 OK を返します。
  2. イベントキュー:検証済みのペイロードを分散イベントブローカー(Apache KafkaやAWS SQSなど)にパブリッシュします。
  3. 入札およびアナリティクスワーカー:イベントストリームを消費し、コンバージョン値を収益指標にマッピングして、リアルタイム入札(RTB)のターゲットCPAモデルを更新します。

階層型ソース識別子の活用

最初の獲得ポストバックでは、ポストバックデータティアに応じて、階層型 source-identifier の2桁、3桁、または4桁が公開される場合があります。これらの桁のセマンティックな意味は、広告ネットワーク独自のソース識別子タクソノミによって定義されます。入札システムは、桁数と特定の配置やクリエイティブの粒度との間に普遍的なマッピングを仮定するのではなく、受信したソース識別子をネットワーク独自のキャンペーンメタデータに対して解決する必要があります。

比較マトリックス:Appleによる直接配信とMMP S2S取り込みの比較

機能的側面 Appleによる直接配信(広告ネットワーク) デベロッパーエンドポイント(NSAdvertising... MMP S2S取り込みパイプライン
受信者 登録済み広告ネットワーク 宣伝対象アプリのデベロッパー モバイル計測パートナー
アトリビューションの範囲 そのネットワークに対する獲得ポストバック アプリに対する獲得ポストバックのコピー マルチネットワークの集約ビュー
署名検証 広告ネットワークのバックエンドで実行 デベロッパーのバックエンドで実行 実装依存(パートナーのフロー)
非獲得ポストバック 条件を満たしている場合に受信(did-win: false デベロッパーエンドポイントには配信されない パートナーのフローを通じて利用可能な場合がある
主なユースケース 直接入札およびターゲットCPAの最適化 内部データウェアハウスの監査と検証 クロスチャネルのパフォーマンスダッシュボード

よくある質問(FAQ)

Appleのポストバック署名の検証にはどの公開鍵が使用されますか?
Appleは、SKAdNetwork 2.1以降のインストール検証に使用される公式のNIST P-256公開鍵をデベロッパー向けドキュメントで公開しています。取り込みサーバーは、この公開鍵をX.509/DER形式で読み込んで、着信した署名を検証します。
有効なSKAdNetworkポストバックの署名検証が失敗するのはなぜですか?
署名検証の失敗は通常、シリアライズエラーが原因で発生します。間違った区切り文字の使用(`\u2063` の代わりに `\u2060` を使用)、不適切なパラメータの順序、SKAN 4のメッセージ文字列へのコンバージョン値の誤った追加、あるいはブール値の文字列表記(`"true"` 対 `"false"`)の誤った処理などが挙げられます。
宣伝対象アプリのデベロッパーエンドポイントで、非獲得のSKAdNetworkポストバックを受信することはできますか?
いいえ。デベロッパー用のコピーエンドポイントには、設定されている場合に獲得インストール検証ポストバックのコピーが届きます。最大5つの非獲得ポストバック(`did-win: false`)は、デベロッパー用コピーエンドポイントではなく、他の適格な広告ネットワークに直接送信されます。

まとめと決定フレームワーク

大規模なSKAdNetworkポストバックの処理には、低レイテンシのエッジ取り込みと、厳格な暗号学的検証およびトランザクションレベルの重複排除の組み合わせが必要です。Appleのポストバックは予算配分や入札アルゴリズムに直接影響を与えるため、ECDSA署名の検証と transaction-id のべき等の強制により、偽造または改ざんされたポストバックや重複したリプレイ処理から取り込みパイプラインを保護できます。

プラットフォームを介したSKAdNetworkレポートを、ミクロレベルのユーザーオンボーディングやインスタントディープリンクルーティングで補完するため、エンジニアリングチームはプラットフォームAPIと並行してファーストパーティのルーティングアーキテクチャを導入しています。

サーバーサイドのアトリビューションポストバックとディープリンクパイプラインの設定の詳細については、OpoInstall ドキュメントを参照してください。

関連資料

  • 概念: S2Sポストバック、暗号学的検証、ECDSA P-256、リプレイ攻撃対策、トランザクション重複排除

  • テクノロジー: Apple SKAdNetwork、Apple AdAttributionKit、Redisインメモリキャッシュ、OpoInstallモバイルSDK

  • 標準規格: IETF RFC 8259(JSONデータ交換)、RFC 5480(楕円曲線暗号)

  • API: StoreKit SKAdNetwork API、Apple S2Sポストバック配信仕様、OpoInstall S2S API

公式ドキュメント

Share this article