App Store Server Notifications V2をGoで検証するOSSを作った

ぺりー
Eureka Engineering
Published in
15 min readJul 21, 2022
https://jwt.io/

こんにちは!Eureka Backend Engineer 23年入社予定のぺりーです。

WWDC22が開催されたばかりですがWWDC21で発表された App Store Server Notifications V2の実装記事が英語、日本語ともにとても少なかったため、検証に使用できるOSSライブラリasnを公開しました。

App Store Server Notifications V2とは

App Store Server Notificationsでは アプリ内課金や返金をリアルタイムで通知するサーバー間サービスです。
トランザクションが変化した際にリアルタイムに通知を受け取ることで、ユーザーがオフラインであってもユーザーアカウントを更新したり、アプリ内課金の返金に対応したり、分析に使用することができます。

App Store Connectにてv1を受け取るように設定することもできますが、今後v1はdeprecatedとなる可能性も高いため、Eurekaではv2を実装することにしました。
実際にWWDCでは以下のように発言しています。

Our goal for this year is to make App Store server notifications even more powerful by making use of our new, easy-to-use signed transactions. In addition to this, we will update the notifications to make sure only on notification is sent for one user action, we will update the payload and the entire payload will be signed using JWS to enhance security.

ドキュメントは全て英語だったため、軽く概要をまとめておきます。
最新版とは異なる場合があるので公式を必ず参照ください。

執筆時の最新バージョンは2021年10月に発表されたv2で、v1と比べて多くのイベント通知を受け取ることができます。

v1とv2の変更点

v2では廃止された通知タイプ4件とと新しく追加された通知タイプ4件があります。
(廃止された通知タイプは後述のサブステートフィールドへ移行されているため、実質的にはv2ではイベントが追加され、廃止されたものはありません。)
下記の差分をご覧ください。

v1とv2の通知タイプの差分

より詳しくユーザーアクションが受け取れるよう、サブステートフィールドが追加されました。
例えば、通知タイプのSUBSCRIBEDではINITIAL_BUY, RESUBSCRIBEの二つのサブステートが送信されます。

v2の通知タイプとサブステート一覧

v2の15種類の通知タイプとサブステートは以下をご覧ください。

App Storeから送信されるJWS

送信されるのは以下のようなJSONでApp Storeによって署名されたJWSのフォーマットのペイロードです。

{“signedPayload”:”…”}

JWSとはデジタル署名またはMACsで改ざんされていないことを保証する形式でJSONベースのデータ構造で表現されています。
(今回JWSのペイロードがClaim Set(JSONオブジェクト)であるためJWTでもあります。)

JWSは署名方式を定めたJOSE Header, 署名付きコンテンツのJWS Payload, 署名のJWS Signatureで構成され、JOSE HeaderはJWS Protected Header, JWS Unprotected Headerの組み合わせです。
JWSにはコンパクトでURLセーフなJWS Compact SerializationとJSON Serializationの二つのシリアライゼーションが定義されています。
どちらのシリアライゼーションもそれぞれbase64でエンコードされます。
ref: https://datatracker.ietf.org/doc/html/rfc7515#section-2

JWS Compact SerializationはJWS Unprotected Headerは使用されず、JOSE HeaderとJWS Protected Headerは同一になります。
以下の形式で表されます。

BASE64URL(UTF8(JWS Protected Header)) || ‘.’ ||
BASE64URL(JWS Payload) || ‘.’ ||
BASE64URL(JWS Signature)

ref: https://datatracker.ietf.org/doc/html/rfc7515#section-3.1

JWS JSON serializationでは以下のメンバが含まれています。

  • payload
    必ず存在してbase64(JWS Payload)を必ず含む
  • signatures
    必ずJSONオブジェクトの配列で下記の要素を含む
    — protected
    JWS Protected Headerが空の場合は必ず省略され、そうでない場合は必ず存在しbase64url(UTF8(JWS Protected Header))を必ず含む
    — header
    JWS Unprotected Headerが空の場合は必ず省略され、そうでない場合はJWS Unprotected HeaderエンコードされていないJSONオブジェクトとして表現される
    — signature
    必ず存在してbase64url(JWS Signature)を必ず含む
    https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.1

signedPayloadをデコードしたresponseBodyV2DecodedPayloadのdata(JSONオブジェクト)に通知タイプに応じてsignedRenewalInfosignedTransactionInfoプロパティを含んでいて、それぞれJWS形式となっています。
responseBodyV2DecodedPayloadは通知タイプ・サブタイプその他のメタデータ、signedRenewalInfoは更新した際の情報、signedTransactionInfoはトランザクションに関する情報が含まれています。

処理の流れ

以上を踏まえると処理の流れは以下のようになります。

  1. signedPayloadの署名を検証する
  2. payloadをbase64urlデコードしresponseBodyV2DecodedPayloadを取得する
  3. 2でデコードしたペイロードのdata(JSONオブジェクト)のsignedTransactionInfo(JWSTransaction), signedRenewalInfo(JWSRenewalInfo)プロパティのそれぞれの署名を検証してbase64urlでデコードする

Headerのalg, x5cプロパティはそれぞれ署名に使用したアルゴリズム、署名した鍵に対応するX.509証明書チェーンが含まれています。
ref: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6

証明書チェーンは署名検証に使用する公開鍵がさらに上位の証明書を署名するチェーン構造です。
ざっくり説明するとSubjectが発行したPublic KeyをIssuerが証明しており、この証明書が正しいことを確認するために署名を検証する必要がありますが、IssuerのPublic Keyを知る必要があり、それを示す別の証明書が必要になるのでチェーン構造になります。
最後の証明書はIssuer, Subjectが同一である自己証明書(ルート証明書)となっています。

証明書チェーンイメージ図

OpenSSLによる検証方法

openssl x509 -inform der -in xxx.cer -out xxx.pem

まずは証明書をpemに変換し、以下のコマンドで検証してみます。

openssl verify -CAfile 0番目のPEM> -untrusted <2番目のPEM> <1番目のPEM>

有効な証明書であることがわかったので、それぞれの証明書からSubject, Issuerを抽出していきます。

openssl x509 -in ICA0.cer -text -noout -inform der

Issuer: C=US, O=Apple Inc., OU=G6, CN=Apple Worldwide Developer Relations Certification Authority
Subject: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Prod ECC Mac App Store and iTunes Store Receipt

openssl x509 -in ICA1.cer -text -noout -inform der`

Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA — G3
Subject: C=US, O=Apple Inc., OU=G6, CN=Apple Worldwide Developer Relations Certification Authority

openssl x509 -in ICA2.cer -text -noout -inform der

Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA — G3
Subject: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA — G3

最後の証明書は、IssuerとSubject(発行者と主体者)が同じなのでルート証明書であることがわかります。

このようにみていくと最初の証明書の発行者が中間の証明書の主体者と同じで、中間の証明書の発行者が最後の証明書の主体者になっていることが確認できます。
先述のイメージ図に当てはめると、最初の証明書がCで中間の証明書がB、最後の証明書がAです。

証明書チェーンイメージ図

Payloadを見てみる

先述した通り、’.’で区切られているため、’.’区切りでBase64デコードすれば、Payloadを見ることができます。

cut -f 1 -d "." FILENAME | base64 -d

{“alg”:”ES256",”x5c”:[“MIIEMDC…”]}

Payloadを実際に見てみると、algにES256が指定されていたため、ECDSA方式の公開鍵が必要なことがわかりました。
x5cはX.509証明書または証明書チェーンがJSON 配列形式でDER形式をbase64エンコードされたものが入っています。
https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6

実装方法

Apple PKIで提供されるルート証明書のうちECDSA形式なものは Apple Root CA - G3 Root のみだったため、そのルート証明書を使って証明書チェーンを検証します。
厳密にはどのalgが指定されるかはHeaderを見ないとわからないため、全タイプのルート証明書で検証する必要があります。
また、PKIで中間証明書も公開されているため、そちらをソースコードに含めることもできます。
その場合は中間証明書でパースする必要がなく、バイト文字列を比較するだけですみます。
しかし、ルート証明書だけ正当であることがわかればチェーン形式で後続の証明書を検証できるため、今回はルート証明書のみをソースコードに置くことにしました。
実際、中間証明書はローテーションがルート証明書よりも早いため、パフォーマンスは多少良くなるかもしれませんが、メンテナンスコストも高くなります。

公開したライブラリ

今回作成したOSSのライブラリでは、一つ以上のルート証明書をバイト文字列や、HTTPリクエスト、 ファイルから取得して、検証することができます。
少し前で述べたように、複数のルート証明書を用意して検証することも必要な可能性があるため、複数のルート証明書を配置できるようにしてあります。

内部ではjwtだけでなくjws, jwk, jweなどを一つのライブラリでカバーし、かつ拡張性があるMakiさんjwxを使用し、jwxのKeyProviderインターフェースのFetchKeysを実装することでx5cを解体した0番目のチェーンをパースして公開鍵を取り出す実装をしています。(前述Aの公開鍵)
詳しくは下記リンクからご覧ください。

https://github.com/satorunooshie/asn/blob/main/key_provider.go

余談

Apple の Sandbox 環境では、一部の通知イベントが対応しておらず、localtunnelを使うなどして実際に繋げてみるしかありませんでした。

現在は、通知イベントにTESTをサポートしているので、そこら辺は楽になったかもしれません。

また、知らぬ間にAppleがDocumentが更新することもあるため、json.Marshalできなかったフィールドを保存できるように工夫しています。(実際に実装中にフィールドが一つ増えていました)
ref: https://pkg.go.dev/encoding/json#RawMessage

実装するにあたってdaisuzuにとてもお世話になりました。
ありがとうございます!!

参考

--

--

ぺりー
Eureka Engineering

Satoru Kitaguchi. Backend Engineer at eureka, Inc.