phpでApp Store Server Notifications v2のJWSを検証する

StoreKit2が発表されたのと同じタイミングで、App Store Server Notifications v2が発表されました。
App Store Server Notifications | Apple Developer Documentation
この通知ではAppleから様々な情報を受け取ることができます。利用する際には通知が本当にAppleから来たものか確認するためにJWSの検証を行う必要があります。
ここではphpでどのように検証を行うかについて整理します。内容に間違いがありましたらコメントお願いします。

テスト通知送信

通知の内容をv1にするかv2にするかはApp Store Connectで切り替えることが可能です。テスト課金を行って通知を飛ばす場合、あらかじめ切り替えておきましょう。
また、App Store Connectの設定をv1にしたままでもRequest a Test Notification APIにより通知を飛ばすとv2仕様で通知が飛ぶようです。まずはこれを実行することでテスト通知を送信しつつ検証をすすめるのも良いでしょう。
Request a Test Notification | Apple Developer Documentation

手順

通知が送信されるとこのようなデータを受信できます。
{ "signedPayload": "eyJh ..."}
signedPayload | Apple Developer Documentation
signedPayloadの中身をドットで区切り、それぞれbase64 decodeするとで先頭から順にheader, payload, signatureを得ます。
$contents = '{"signedPayload":"eyJh ... ";
$signedPayload = json_decode($contents, true)['signedPayload'];
list($jws_header, $jws_body, $jws_signature) = explode(".", $signedPayload);
$header = json_decode(base64_decode($jws_header), true);
$payload = json_decode(base64_decode($jws_body), true);
$signature = base64_decode($jws_signature);
まずheaderから証明書を取り出し、その証明書を変換しておきます。
$chain_array = $header['x5c'];
$pem_from_x5c_cert_list = array();
foreach ($chain_array as $key => $chain_array_item) {
    $pem_from_x5c_cert = (
        "-----BEGIN CERTIFICATE-----\n" .
        chunk_split($chain_array_item, 64, "\n") .
        "-----END CERTIFICATE-----\n"
    );
    $pem_from_x5c_cert_list[] = $pem_from_x5c_cert;
    //証明書の内容を確認する場合
    $chain_cert = openssl_x509_parse(openssl_x509_read($pem_from_x5c_cert));
    echo('証明書の内容 : ' . PHP_EOL);
    echo ('name : ' . $chain_cert['name'] . PHP_EOL);
}
データの中身はpayloadに入っています。 そのデータについてまずsignatureで検証します。ここではfirebase/php-jwtを使いました。
php-jwt/JWT.php at 018dfc4e1da92ad8a1b90adc4893f476a3b41cb8 · firebase/php-jwt
(あらかじめ)
use \Firebase\JWT\JWT;
(コード)
$jwt_decode_result = JWT::decode($signedPayload, new \Firebase\JWT\Key($pem_from_x5c_cert_list[0], 'ES256'));
var_dump($jwt_decode_result);

次に証明書チェーンの検証を行います。
まずAppleのルート証明書を用意します。ダウンロードして.cerを.pemに変換します。Appleのルート証明書が更新される可能性があるのでそこに注意する必要がありますが、このコードのように毎回ダウンロードする必要は無いはずなのであくまでサンプルです。
$apple_cert_url = 'https://www.apple.com/certificateauthority/AppleRootCA-G3.cer';
$apple_cert_content = file_get_contents($apple_cert_url);
$apple_pem =  '-----BEGIN CERTIFICATE-----'.PHP_EOL
    .chunk_split(base64_encode($apple_cert_content), 64, PHP_EOL)
    .'-----END CERTIFICATE-----'.PHP_EOL;
得られたAppleのルート証明書を含め、これらの証明書を順番に検証していきます。 まずappleの証明書を追加しておきます。
$pem_from_x5c_cert_list[] = $apple_pem;

php7.4以上ではopenssl_x509_verify()が使えるため、以下のように検証できます。
PHP: openssl_x509_verify - Manual
//検証
foreach ($chain_array as $key => $chain_array_item) {
    $result = openssl_x509_verify($pem_from_x5c_cert_list[$key + 1], openssl_get_publickey($pem_from_x5c_cert));
    var_dump($result);
    echo $result ? 'valid'.PHP_EOL : 'invalid'.PHP_EOL;
}
php7.4未満のバージョンの場合、phpseclibのv3を使い以下のように書けます。
(あらかじめ)
use phpseclib\File\X509;
(コード)
for($i = 0; $i < (count($pem_from_x5c_cert_list) - 1); $i++) {
    $cert = $pem_from_x5c_cert_list[$i];
    $x509 = new X509();
    $x509->loadCA($pem_from_x5c_cert_list[$i+1]);
    $cert = $x509->loadX509($cert);
    var_dump($x509->validateSignature());
    echo $x509->validateSignature() ? 'valid'.PHP_EOL : 'invalid'.PHP_EOL;
}
これらの検証が全部okになる必要があります。

まとめ

App Store Server Notificationsの通知のJWT検証をphpで行いました。これらの手順を行ってから通知の中身に入る必要があります。Appleからはv1廃止時期はアナウンスされていませんが今後はv2が必須になりそうなため、早めに対応しておきましょう。

参考文献

App Store Server Notifications Version 2(StoreKit 2)の JWS を検証する | by Taiga ASANO | mixi developers
Validate StoreKit2 in-app purchase… | Apple Developer Forums
phpでSafetyNet APIのレスポンスの証明書を検証する | GRIPHONE ENGINEER'S BLOG

PythonでCSA形式の棋譜データを読み込む

pythonで棋譜データを扱う

棋譜データをpythonで読み込む方法についてのメモです。こちらのライブラリを使用させて頂いています。
GitHub - gunyarakun/python-shogi: A pure Python shogi library with move generation and validation and handling of common formats.

元となる棋譜データはCSA形式を想定しています。サンプルデータには「将棋DB2」のCSA形式ファイルを使用しました。
羽生善治 vs. 藤井猛 順位戦 - 無料の棋譜サービス 将棋DB2

サンプルコード

import shogi.CSA

kif = shogi.CSA.Parser.parse_file('test.csa')[0]

board = shogi.Board()
for move in kif['moves']:
    # どこからどこに移動したか USIプロトコル形式
    print(move)
    # 移動
    board.push_usi(move)

    # 盤面表示
    print(board) # テキスト形式(ASCII文字)
    print(board.kif_str()) # テキスト形式(KIFスタイル)

    # 占有している駒の位置 右下から左に移動という方向で表記されている
    print('{:0=81b}'.format(board.occupied[shogi.BLACK]))# 先手
    print('{:0=81b}'.format(board.occupied[shogi.WHITE]))# 後手
    
    # 先手の持ち駒 参考 : https://tadaoyamaoka.hatenablog.com/entry/2017/04/08/165742
    hand = board.pieces_in_hand
    hand_b = hand[shogi.BLACK]
    for p in hand_b.keys():
        print("piece: {0}, num: {1}".format(shogi.Piece(p, shogi.BLACK).symbol(), hand_b[p]))
        print("piece: {0}, num: {1}".format(shogi.Piece(p, shogi.BLACK).japanese_symbol(), hand_b[p]))

    print('-'*100)

USI形式について

駒の表記法、位置の表記法についてはこちらをご覧ください
将棋所:USIプロトコルとは

駒の表記法

盤面を表記するとき、1段目の左側(9筋側)から駒の種類を書いていきます。空白の升は、空白が続く個数の数字を書きます。
平手初期局面の場合、1段目は、左から後手の駒が香桂銀金玉金銀桂香と並んでいるので、lnsgkgsnlとなります。2段目は、空白が1升、後手の飛車、空白が5升、後手の角、空白が1升というように並んでいるので、1r5b1となります。

位置の表記法

指し手に関しては、駒の移動元の位置と移動先の位置を並べて書きます。7七の駒が7六に移動したのであれば、7g7fと表記します。(駒の種類を表記する必要はありません。)
駒が成るときは、最後に+を追加します。8八の駒が2二に移動して成るなら8h2b+と表記します。
持ち駒を打つときは、最初に駒の種類を大文字で書き、それに*を追加し、さらに打った場所を追加します。金を5二に打つ場合はG*5bとなります

Python MidoでのMIDIファイル生成方法メモ

Mido

midoはMIDIを扱うことができるpythonライブラリです。
このライブラリを使うことでMIDIファイルの読み書きが可能です。今回はpython3で音を指定してMIDIファイルを作成するmidoの使い方についてメモしていきます。

基本形

まずはこのドキュメント通りに出力して、noteの値を変えて音が変わることを確認してみましょう。
MIDI Files — Mido 1.2.10 documentation
import mido
from mido import Message, MidiFile, MidiTrack

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)

# MIDIの音色を指定
track.append(Message('program_change', program=12, time=0))
# C3(note=60, ド)の音を、指定の音量と時間で再生
track.append(Message('note_on', note=60, velocity=64, time=32))
track.append(Message('note_off', note=60, velocity=127, time=32))

mid.save('new_song.mid')
※Messageに指定する値と範囲はこちらのドキュメントを参照してください。
Message Types — Mido 1.2.10 documentation

テンポを指定するにはset_tempoを使います。4分音符の長さを480tickとして、tick数により長さを指定します。
テンポを指定しつつ、いくつかの音を出してみましょう。
import mido
from mido import Message, MidiFile, MidiTrack, MetaMessage

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120)))

track.append(Message('note_on', note=60, velocity=64, time=0))
track.append(Message('note_off', note=60, time=480))
track.append(Message('note_on', note=60+4, velocity=64, time=0))
track.append(Message('note_off', note=60+4, time=480))
track.append(Message('note_on', note=60+7, velocity=64, time=0))
track.append(Message('note_off', note=60+7, time=480))

mid.save('new_song.mid')
このような内容のmidiが出力されます。
sample2_midi

和音の出し方

音を重ねるにはこのように指定します。
import mido
from mido import Message, MidiFile, MidiTrack, MetaMessage

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120)))

track.append(Message('note_on', note=64, velocity=60, time=0))
track.append(Message('note_on', note=64+4, velocity=60, time=0))
track.append(Message('note_on', note=64+7, velocity=60, time=0))
track.append(Message('note_off', note=64, time=480))
track.append(Message('note_off', note=64+4, time=0))
track.append(Message('note_off', note=64+7, time=0))

mid.save('new_song.mid')
sample3_midi

複数のトラックの作成

トラックを複数用意し、それぞれに音を入れていきましょう。
import mido
from mido import Message, MidiFile, MidiTrack, MetaMessage

mid = MidiFile()
track1 = MidiTrack()
track2 = MidiTrack()
mid.tracks.append(track1)
mid.tracks.append(track2)
track1.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120)))
track2.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120)))

track1.append(Message('note_on', note=64, velocity=60, time=0))
track1.append(Message('note_on', note=64+7, velocity=60, time=0))
track1.append(Message('note_off', note=64, time=480))
track1.append(Message('note_off', note=64+7, time=0))

track2.append(Message('note_on', note=64+4, velocity=60, time=40))
track2.append(Message('note_off', note=64+4, time=480))

mid.save('new_song.mid')

複雑な音階

midoはプログラムの中で扱えるので、このように適当に変数を使いつつ組み立てていくことになるでしょう。今回はバッハ 平均律クラヴィーア曲集 第1巻 みたいな音を作ります。
ただしコードを見て分かるように、読みやすいものを作るのは難易度が高そうです。
import mido
from mido import Message, MidiFile, MidiTrack, MetaMessage

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120)))

node_time = 480 * 2
node_pos = 60
node_16 = int(node_time/8)

track.append(Message('note_on', note=node_pos, velocity=60, time=0))
track.append(Message('note_on', note=node_pos+4, velocity=60, time=node_16 ))
track.append(Message('note_on', note=node_pos+7, velocity=60, time=node_16 ))
track.append(Message('note_off', note=node_pos+7, velocity=60, time=node_16 ))
track.append(Message('note_on', note=node_pos+12, velocity=60, time=0 ))
track.append(Message('note_off', note=node_pos+12, velocity=60, time=node_16 ))
track.append(Message('note_on', note=node_pos+16, velocity=60, time=0 ))
track.append(Message('note_off', note=node_pos+16, velocity=60, time=node_16 ))
track.append(Message('note_on', note=node_pos+7, velocity=60, time=0 ))
track.append(Message('note_off', note=node_pos+7, velocity=60, time=node_16 ))
track.append(Message('note_on', note=node_pos+12, velocity=60, time=0 ))
track.append(Message('note_off', note=node_pos+12, velocity=60, time=node_16 ))
track.append(Message('note_on', note=node_pos+16, velocity=60, time=0 ))
track.append(Message('note_off', note=node_pos+16, velocity=60, time=node_16 ))
track.append(Message('note_off', note=node_pos+4, velocity=60, time=0 ))
track.append(Message('note_off', note=node_pos, velocity=60, time=0))

mid.save('new_song.mid')
このような内容が出力されます。
sample4_midi
これを使った作例がこちらです。
バッハの平均律クラヴィーアみたいのをランダム生成する - えんたつの記録

このようにMidoはプログラムでMidi生成することにも活用できます。
このサイトについて
Webアプリケーション開発のことや、iPhone・Android向けアプリ開発の話題が中心です。
管理:えんたつ twitter: @tattyamm
mimage
一部のリンクにはアフィリエイトが含まれます。
カテゴリ別アーカイブ
RSS
プログラミング本
古い本含めてメモです
iPhoneプログラミングUIKit詳解リファレンス iPhoneプログラミングUIKit詳解リファレンス Android Layout Cookbook アプリの価値を高める開発テクニック パーフェクトPHP (PERFECT SERIES 3) JavaプログラミングBlack Book 2nd Edition (Black Bookシリーズ)
表記
当サイトではGoogle Analyticsを使用しております。詳細はこちらを御覧ください