新型コロナワクチン接種証明書アプリで発行されたQRコードをPythonで読んでみる
接種証明書アプリ、PINを入力してマイナンバーカードをかざすだけで問い合わせ、発行ができてとても便利ですね。国の省庁が個人に対してこんなにサクッと証明書を発行できるという前例を作れたのはとても良いことだと思います。
発行される Credential は Verifiable
ところで、新型コロナワクチン接種証明書アプリで発行されたQRコードに記載されている内容がVerifiable Credentialということで、ちょっと話題になっています。
Verifiable Credentialは証明内容をセキュアに、機械可読な形式で配布できるように方式を定めたものです。
今回の接種証明書アプリで発行される証明書は、発行形態の1つとして、JSON形式で記載された内容を検証可能なように記述する JWS
という仕様で表現された Verifiable Credential が使われています。
接種証明?
接種証明書アプリでひときわ目を引くQRコードは SMART Health Cards の仕様に基づいて発行されている証明書です。QRコード表示画面にも「SMART」とありますね。
で、このQRコードから読める文言に「ちゃんと厚生労働省が発行したという証拠」が含まれていて、証明書を検証することができるというわけです(持ち主が本当にその人かは別で確認)。
実際に読んでみると shc:/
から始まるスキームの文字列が出てきます。
shc スキーム
この shc:/
から始まる文字列の仕様は以下で確認することができました。
https://spec.smarthealth.cards/#encoding-chunks-as-qr-codes
パータンとしては、 shc:/<数字の連続>
shc:/<数字:C>/<数字:N>/<数字の連続>
という形になっているようです。2つ目のパターンは N分のC番目
といった感じで複数ある場合にとられる形式のようです。
(自分のQRコードでは1つ目のパターンでした)
数字が表すもの
上記で <数字の連続>
と書きましたが、これはJWS(Json Web Signature)という署名付きJSONデータを以下のやり方でエンコードしたものです。
Each character "c" of the JWS is converted into a sequence of two digits as by taking Ord(c)-45 and treating the result as a two-digit base ten number.
(https://spec.smarthealth.cards/#encoding-chunks-as-qr-codes より引用)
Ord
とは ASCIIコードだと思うのですが、-(ハイフン)
記号が45なので、各文字のASCIIコードの値から45引くということをしています。
こうすることで、ハイフンを 00
として、各文字が2ケタの10進数で表せるようにできるようです。
よって、これをエンコードしていくには 2ケタごとに数字としてよみとって、 +45したものをASCIIコード変換で戻してあげることででき、JWSに戻すことができます。
デコードして出てきた JWS (コロナワクチン接種証明書の場合)
JWSとは ヘッダ
ペイロード
シグネチャ
を .(ドット)
でつないだものです。ヘッダはBase64でエンコードされたJSONなので、最初のドットまでの部分の文字列を Base64 でデコードしてあげるとJSON形式のヘッダが出てきます。
{"zip": "DEF", "alg": "ES256", "kid": "f1vhQP9oOZkityrguynQqB4aVh8u9xcf3wm4AFF4aVw"}
私の場合はこんな感じでした。 alg
と kid
はシグネチャがどの鍵を用いて計算されたかを表し、 "zip": "DEF"
とあるのはペイロードがDEFLATEで圧縮されているということを表しています。
署名検証用の鍵
シグネチャを検証するためには公開鍵が必要になってくるわけですが、それは以下から取ってくることができます。JWKSとは「Json Web Key Set」のことで、署名に使用された鍵の公開鍵一覧です。 kid
と alg
から一致する鍵を取ってくることができれば署名検証できます。
https://vc.vrs.digital.go.jp/issuer/.well-known/jwks.json
なお、このURLはペイロードに含まれる iss
のURLから作れます。
ペイロードの形式
今回のペイロードはヘッダにもあったようにDEFLATEで圧縮されたものでしたが、以下にあるようにSHCの仕様的にも DEFLATE アルゴリズムで圧縮されたものであることとされています。(そしてzlibだったりgzヘッダは取り除くべき、とあります)
payload is compressed with the DEFLATE (see RFC1951) algorithm before being signed (note, this should be "raw" DEFLATE compression, omitting any zlib or gz headers)
(https://spec.smarthealth.cards/#health-cards-are-small より引用)
Python でパースしてみる
上記で調べた内容をもとにパースしてみました。
コロナワクチンの接種証明書のQRコード内容を python でパースしてみた。JOSEが身近になった気がする
— いしぐ (@yoh1496) 2021年12月22日
ちな署名検証用の公開鍵(JSON Web Key Set)は iss のURLに /.well-known/jwks.json くっつけたURLでとれるっぽい。https://t.co/0uM8C1IIwl pic.twitter.com/nZVie8qi7o
import zlib, base64 strSHC = "shc:/567629095243206034602..." #paste here # decode arrChar = [] it = iter(strSHC.replace("shc:/", "")) for i, j in zip(it, it): arrChar.append(chr(int(i) * 10 + int(j) + 45)) # Header strHeader = ''.join(arrChar).split('.')[0].replace('-', '+').replace('_', '\/') print(base64.b64decode(strHeader + "=" * (len(strHeader) % 4))) # Payload strPayload = ''.join(arrChar).split('.')[1].replace('-', '+').replace('_', '\/') bytesPayload = base64.b64decode(strPayload + "=" * (len(strPayload) % 4)) print(zlib.decompress(bytesPayload, -15))
署名を検証する
署名検証については一般的なJWSの検証方法でいけると考えられますので、私の生半可な理解で書かれた内容よりはそちらを参照していただきたく、、、
終わりに
OpenID Connect の id token に使用されている JWT をはじめとし、最近JOSEについてちょっと詳しくなれたような気がしました。解説を読むよりも手でコードを書いてパースしてみた方が腹落ちしやすい自分にとってはよい題材だったと思います。
「ASCIIコードから45を引いてBASE64を2ケタの10進数で表現する」というテクニックはどうもQRコード向けの容量削減テクニックらしく、そういうのも楽しいなと思いました。
どなたかの参考になれば幸いです。