いしぐめも

プログラミングとかしたことを書きます。

新型コロナワクチン接種証明書アプリで発行されたQRコードをPythonで読んでみる

接種証明書アプリ、PINを入力してマイナンバーカードをかざすだけで問い合わせ、発行ができてとても便利ですね。国の省庁が個人に対してこんなにサクッと証明書を発行できるという前例を作れたのはとても良いことだと思います。

発行される Credential は Verifiable

ところで、新型コロナワクチン接種証明書アプリで発行されたQRコードに記載されている内容がVerifiable Credentialということで、ちょっと話題になっています。

w3c.github.io

Verifiable Credentialは証明内容をセキュアに、機械可読な形式で配布できるように方式を定めたものです。

今回の接種証明書アプリで発行される証明書は、発行形態の1つとして、JSON形式で記載された内容を検証可能なように記述する JWS という仕様で表現された Verifiable Credential が使われています。

datatracker.ietf.org

接種証明?

接種証明書アプリでひときわ目を引くQRコードは SMART Health Cards の仕様に基づいて発行されている証明書です。QRコード表示画面にも「SMART」とありますね。

f:id:yoh1496:20211222145403p:plain
証明書のQRコード

で、この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"}

私の場合はこんな感じでした。 algkidシグネチャがどの鍵を用いて計算されたかを表し、 "zip": "DEF" とあるのはペイロードがDEFLATEで圧縮されているということを表しています。

署名検証用の鍵

シグネチャを検証するためには公開鍵が必要になってくるわけですが、それは以下から取ってくることができます。JWKSとは「Json Web Key Set」のことで、署名に使用された鍵の公開鍵一覧です。 kidalg から一致する鍵を取ってくることができれば署名検証できます。

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 でパースしてみる

上記で調べた内容をもとにパースしてみました。

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コード向けの容量削減テクニックらしく、そういうのも楽しいなと思いました。

どなたかの参考になれば幸いです。