いしぐめも

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

新型コロナワクチン接種証明書の署名をpythonで検証する

SMART Health Cards (SHC) はJSON Web Signature (JSON Web Signature) という形式で表されてからQRコード化されるため、そのQRコードを読み取った人が「接種証明書に記載されている発行元が発行したものか」「発行されてから改ざんされていないか」を検証できる「署名」を含みます。 今回はその署名データをpythonを用いて検証してみたいと思います。

SMART Health Cards は検証可能

前回記事では、pythonを使って「新型コロナワクチン接種証明書」のSMART Health Cards をパースしてみたという内容を書きました。検証方法について省略してしまったので、今回はそこらへんのコードを示したいと思います。

パースなど前回の内容はコチラ↓

yoh1496.hatenablog.com

JWS 概要

SHC で使用されている JWS は「Compact Serialization」と呼ばれる方式で表現された JWS であり、以下の3パートを base64urlエンコードし、 .(ドット)で連結した 文字列です。

  • ペイロードの表現方法や、署名方式を定めた「ヘッダ」
  • 署名付きコンテンツとして表現したい内容を記載した「ペイロード
  • 署名

概要としてはこんな感じで、詳細は以下の RFC7515 を参照してください。

datatracker.ietf.org

JWS は有名なところでいくと OpenID Connect の IDトークンで使用されており、ライブラリもたくさん開発されているのでそれらを使えば一発で検証できるのですが、 ここではあえて python の暗号ライブラリを使用して署名を検証してみます。

JWS の署名

JWSにどういう署名を入れるか、という内容はアプリケーションで自由に決められるようになっていて、今回扱う SHC では、ペイロードに示された issuer に JWKS (Json Web Key Set) のエンドポイントを用意して、 そこから公開鍵を取り寄せて検証するという仕様になっています。

今回の Issuer は https://vc.vrs.digital.go.jp/issuer であり、公開鍵を公開しているエンドポイントは、URLに 「/.well-known/jwks.json」を付与した https://vc.vrs.digital.go.jp/issuer/.well-known/jwks.json になっています。

今回の新型コロナワクチン接種証明書のヘッダは以下の通りなので、(前回記事 参照)

{"zip": "DEF", "alg": "ES256", "kid": "f1vhQP9oOZkityrguynQqB4aVh8u9xcf3wm4AFF4aVw"}

公開鍵エンドポイントから alg と kid が一致するものを見つけてくると、使用する公開鍵はコレ、ということがわかります。(といっても執筆時点での鍵は1種類しかありませんが)

{
  "kty": "EC",
  "kid": "f1vhQP9oOZkityrguynQqB4aVh8u9xcf3wm4AFF4aVw",
  "use": "sig",
  "alg": "ES256",
  "x5c": [
    "MIIByjCCAXGgAwIBAgIJAPZFN9WW4voaMAoGCCqGSM49BAMDMCIxIDAeBgNVBAMMF3ZjLnZycy5kaWdpdGFsLmdvLmpwIENBMB4XDTIxMTEyNTEyNTUxNloXDTIyMTEyNTEyNTUxNlowJjEkMCIGA1UEAwwbdmMudnJzLmRpZ2l0YWwuZ28uanAgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEViKBgZ0f3pQKv+tSz653HUtIzCS8TVSNu1Hwi0tKpSnTXXvtqkpcfYeAZ+SfvVk8SWNaTRDZ9wTNjb9c58v9l6OBizCBiDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUiIXKUyT93YdyqsIjE8i5I1z8w0IwHwYDVR0jBBgwFoAU0cYt0sPpuIDBt7a9PD3qs9mOu7EwLgYDVR0RBCcwJYYjaHR0cHM6Ly92Yy52cnMuZGlnaXRhbC5nby5qcC9pc3N1ZXIwCgYIKoZIzj0EAwMDRwAwRAIgEwVdLdbPqMYqEsVltnsm3bI/Z6eibgMwYaNVZiu0r2sCIFebHk1i6ghWOQn+Q0+t5F77fasgJ3Oc6NWx9I8AWjRM",
    "MIIBkDCCATagAwIBAgIJAOECTZDa4MA7MAoGCCqGSM49BAMEMCcxJTAjBgNVBAMMHHZjLnZycy5kaWdpdGFsLmdvLmpwIFJvb3QgQ0EwHhcNMjExMTI1MTI1NTEzWhcNMjYxMTI0MTI1NTEzWjAiMSAwHgYDVQQDDBd2Yy52cnMuZGlnaXRhbC5nby5qcCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEL3S0yNIJ8EuxgiaHEvsjGWd60P0BBKUfVUVSxpVyGsnXwuzkS7OPGG/DT60m5XTvKT125MRuZoS/sajPBcg2KjUDBOMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNHGLdLD6biAwbe2vTw96rPZjruxMB8GA1UdIwQYMBaAFPKN8VogQyX0IuxEi7jBB5gUnFinMAoGCCqGSM49BAMEA0gAMEUCIQCcq3H/pRMRkUmpWUDsggQXJAjLB/AutlHQigEBsVx0sgIgfVyc0L1cbRaDmdCQ3CGd994rRuwlQI0/cJCIv5LeI3g=",
    "MIIBlTCCATugAwIBAgIJANt2MZrWChe2MAoGCCqGSM49BAMEMCcxJTAjBgNVBAMMHHZjLnZycy5kaWdpdGFsLmdvLmpwIFJvb3QgQ0EwHhcNMjExMTI1MTI1NDUzWhcNMzExMTIzMTI1NDUzWjAnMSUwIwYDVQQDDBx2Yy52cnMuZGlnaXRhbC5nby5qcCBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEilfgw+JIG8TOliOLe7jufm2m0+HqL4t5nvBdQj3UMgh8jjl6VoVKKwcj3T1DWFinm6sCTWYUrPSXWcvOq64GbKNQME4wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQU8o3xWiBDJfQi7ESLuMEHmBScWKcwHwYDVR0jBBgwFoAU8o3xWiBDJfQi7ESLuMEHmBScWKcwCgYIKoZIzj0EAwQDSAAwRQIgQWnKyVhaKpu1WcXP49s9inaa5mnWgV/pCW31h/NIJnwCIQDSIHvGUuPwK+ofYqLJGo99hhwhfkIBWhvSo0vr5IGesg=="
  ],
  "crv": "P-256",
  "x": "ViKBgZ0f3pQKv-tSz653HUtIzCS8TVSNu1Hwi0tKpSk",
  "y": "01177apKXH2HgGfkn71ZPEljWk0Q2fcEzY2_XOfL_Zc"
}

この鍵のフォーマットは RFC7517 「JSON Web Key」で定義されているので、詳細はそちらを参照してください。

datatracker.ietf.org

JWS の署名検証

「署名」がその秘密鍵の持ち主によって施されたものであることを検証するためには、 秘密鍵の「対となる公開鍵」のほかに、署名生成の「元となる文書」が必要です。

JWS では元となる文書は「ヘッダをbase64urlエンコードしたものペイロードをbase64urlエンコードしたもの を ドットでつないだ文字列」としています。つまり、JWSを構成するパートは既にbase64urlエンコードされているので、前半2つの文字列をドットでつなげばオッケーということです。

(正確には RFC7797「JSON Web Signature (JWS) Unencoded Payload Option」 で、base64urlエンコードせずに署名する方法や、JWSにペイロードを含ませない detached payload というのもあるにはあるのですが、今回は割愛します)

署名検証コード

というわけで、 SHCの署名を検証するためのコード例を書いてみました。本来であればヘッダの内容を読んで署名アルゴリズムを切り替えたりしなきゃいけないんですが、大体の雰囲気を掴むための例だと思っていただければ幸いです。

from Crypto.PublicKey import ECC

# Make Public Key from parameters
endian = "big"
x_int = int.from_bytes(base64.urlsafe_b64decode("ViKBgZ0f3pQKv-tSz653HUtIzCS8TVSNu1Hwi0tKpSk" + "===="), endian)
y_int = int.from_bytes(base64.urlsafe_b64decode("01177apKXH2HgGfkn71ZPEljWk0Q2fcEzY2_XOfL_Zc" + "===="), endian)
curve = "P-256"

key = ECC.construct(curve=curve, point_x=x_int, point_y=y_int )

# Calc Hash
from Crypto.Hash import SHA256
hashobj = SHA256.new((strHeader+"."+strPayload).encode("ascii"))

# Signature
strSignature = ''.join(arrChar).split('.')[2]
bytesSignature = base64.urlsafe_b64decode(strSignature + "====")

# Verify Signature 
from Crypto.Signature import DSS
sigobj = DSS.new(key, "fips-186-3")
sigobj.verify(hashobj, bytesSignature)

今回は pycryptodome というライブラリを使用してみました。

実際に検証して思ったこと(知ったこと)

前回記事では「JWSの署名検証なんて誰でもやってるし需要ないだろう」と思っていたんですが、いざやろうとしてみたら思ったより記事がなく困ったので公開するに至りました。

実はJWSの検証は SHC のサンプルをやってみて満足していたんですが、公開鍵の公開って国ごとの裁量になっていて、JWKSのエンドポイントを見てみるだけでも面白いのかもなと思ったりしました。以下所感

デジタル庁のJWKには x5c がついている

署名検証してみようと思いいたるまで見落としていたんですが、デジタル庁発行の公開鍵(JWK)には x5c というパラメータがついています。x5c というのは「 X.509 Certificate Chain 」であり、公開鍵の所有を証明する文書の配列です。

構造としては、「この人がこの公開鍵であることを証明するよ」という文書があって、その署名検証に使用する公開鍵がさらに上位の証明書で署名してあるといった「チェーン」のような構造になっているので「証明書チェーン」と呼ばれています(と思います)。

デジタル庁のJWKの証明書チェーンを見てみる

多分何を言ってるのかは見てみた方が早いのでパースしてみます。とりあえず配列になっているので1つ目から見ていきたいと思います。

"MIIByjCCAXGgAwIBAgIJAPZFN9WW4voaMAoGCCqGSM49BAMDMCIxIDAeBgNVBAMMF3ZjLnZycy5kaWdpdGFsLmdvLmpwIENBMB4XDTIxMTEyNTEyNTUxNloXDTIyMTEyNTEyNTUxNlowJjEkMCIGA1UEAwwbdmMudnJzLmRpZ2l0YWwuZ28uanAgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEViKBgZ0f3pQKv+tSz653HUtIzCS8TVSNu1Hwi0tKpSnTXXvtqkpcfYeAZ+SfvVk8SWNaTRDZ9wTNjb9c58v9l6OBizCBiDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUiIXKUyT93YdyqsIjE8i5I1z8w0IwHwYDVR0jBBgwFoAU0cYt0sPpuIDBt7a9PD3qs9mOu7EwLgYDVR0RBCcwJYYjaHR0cHM6Ly92Yy52cnMuZGlnaXRhbC5nby5qcC9pc3N1ZXIwCgYIKoZIzj0EAwMDRwAwRAIgEwVdLdbPqMYqEsVltnsm3bI/Z6eibgMwYaNVZiu0r2sCIFebHk1i6ghWOQn+Q0+t5F77fasgJ3Oc6NWx9I8AWjRM"

この文字列、 DER 形式のx509証明書をbase64エンコードしたものなので、先頭に -----BEGIN CERTIFICATE-----と末尾に -----END CERTIFICATE----- を入れてあげればそのまま PEM として読めます。(base64デコードしたのをDERとして読んでもよい)

こんな感じ↓

f:id:yoh1496:20220202171659p:plain
PEMファイル

それを openssl で読んでみます。

openssl x509 -text -noout -in example.pem

結果

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            f6:45:37:d5:96:e2:fa:1a
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: CN = vc.vrs.digital.go.jp CA
        Validity
            Not Before: Nov 25 12:55:16 2021 GMT
            Not After : Nov 25 12:55:16 2022 GMT
        Subject: CN = vc.vrs.digital.go.jp Issuer
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:56:22:81:81:9d:1f:de:94:0a:bf:eb:52:cf:ae:
                    77:1d:4b:48:cc:24:bc:4d:54:8d:bb:51:f0:8b:4b:
                    4a:a5:29:d3:5d:7b:ed:aa:4a:5c:7d:87:80:67:e4:
                    9f:bd:59:3c:49:63:5a:4d:10:d9:f7:04:cd:8d:bf:
                    5c:e7:cb:fd:97
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Key Usage:
                Digital Signature
            X509v3 Subject Key Identifier:
                88:85:CA:53:24:FD:DD:87:72:AA:C2:23:13:C8:B9:23:5C:FC:C3:42
            X509v3 Authority Key Identifier:
                keyid:D1:C6:2D:D2:C3:E9:B8:80:C1:B7:B6:BD:3C:3D:EA:B3:D9:8E:BB:B1

            X509v3 Subject Alternative Name:
                URI:https://vc.vrs.digital.go.jp/issuer
    Signature Algorithm: ecdsa-with-SHA384
         30:44:02:20:13:05:5d:2d:d6:cf:a8:c6:2a:12:c5:65:b6:7b:
         26:dd:b2:3f:67:a7:a2:6e:03:30:61:a3:55:66:2b:b4:af:6b:
         02:20:57:9b:1e:4d:62:ea:08:56:39:09:fe:43:4f:ad:e4:5e:
         fb:7d:ab:20:27:73:9c:e8:d5:b1:f4:8f:00:5a:34:4c

この証明書の読み方としては、 「Subject が持つ Public-Key の内容を Issuer が示している」ということです。

f:id:yoh1496:20220202171915p:plain
openssl 結果

そしてこの証明書が正しいことを確認するためには署名を検証する必要があるのですが、それにはこの Issuer の Public-Key を知る必要があり、それを示す別の証明書が必要になります。というわけで次の証明書です。

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            e1:02:4d:90:da:e0:c0:3b
        Signature Algorithm: ecdsa-with-SHA512
        Issuer: CN = vc.vrs.digital.go.jp Root CA
        Validity
            Not Before: Nov 25 12:55:13 2021 GMT
            Not After : Nov 24 12:55:13 2026 GMT
        Subject: CN = vc.vrs.digital.go.jp CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:42:f7:4b:4c:8d:20:9f:04:bb:18:22:68:71:2f:
                    b2:31:96:77:ad:0f:d0:10:4a:51:f5:54:55:2c:69:
                    57:21:ac:9d:7c:2e:ce:44:bb:38:f1:86:fc:34:fa:
                    d2:6e:57:4e:f2:93:d7:6e:4c:46:e6:68:4b:fb:1a:
                    8c:f0:5c:83:62
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:TRUE
            X509v3 Subject Key Identifier:
                D1:C6:2D:D2:C3:E9:B8:80:C1:B7:B6:BD:3C:3D:EA:B3:D9:8E:BB:B1
            X509v3 Authority Key Identifier:
                keyid:F2:8D:F1:5A:20:43:25:F4:22:EC:44:8B:B8:C1:07:98:14:9C:58:A7

    Signature Algorithm: ecdsa-with-SHA512
         30:45:02:21:00:9c:ab:71:ff:a5:13:11:91:49:a9:59:40:ec:
         82:04:17:24:08:cb:07:f0:2e:b6:51:d0:8a:01:01:b1:5c:74:
         b2:02:20:7d:5c:9c:d0:bd:5c:6d:16:83:99:d0:90:dc:21:9d:
         f7:de:2b:46:ec:25:40:8d:3f:70:90:88:bf:92:de:23:78

これを見ると Issuer が vc.vrs.digital.go.jp Root CA 、 Subject が vc.vrs.digital.go.jp CA になっています。1つ目の証明書を見るとIssuerが vc.vrs.digital.go.jp CA となっているので、1つ目の署名検証をするのに使える公開鍵が得られそうです。

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            db:76:31:9a:d6:0a:17:b6
        Signature Algorithm: ecdsa-with-SHA512
        Issuer: CN = vc.vrs.digital.go.jp Root CA
        Validity
            Not Before: Nov 25 12:54:53 2021 GMT
            Not After : Nov 23 12:54:53 2031 GMT
        Subject: CN = vc.vrs.digital.go.jp Root CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:8a:57:e0:c3:e2:48:1b:c4:ce:96:23:8b:7b:b8:
                    ee:7e:6d:a6:d3:e1:ea:2f:8b:79:9e:f0:5d:42:3d:
                    d4:32:08:7c:8e:39:7a:56:85:4a:2b:07:23:dd:3d:
                    43:58:58:a7:9b:ab:02:4d:66:14:ac:f4:97:59:cb:
                    ce:ab:ae:06:6c
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:TRUE
            X509v3 Subject Key Identifier:
                F2:8D:F1:5A:20:43:25:F4:22:EC:44:8B:B8:C1:07:98:14:9C:58:A7
            X509v3 Authority Key Identifier:
                keyid:F2:8D:F1:5A:20:43:25:F4:22:EC:44:8B:B8:C1:07:98:14:9C:58:A7

    Signature Algorithm: ecdsa-with-SHA512
         30:45:02:20:41:69:ca:c9:58:5a:2a:9b:b5:59:c5:cf:e3:db:
         3d:8a:76:9a:e6:69:d6:81:5f:e9:09:6d:f5:87:f3:48:26:7c:
         02:21:00:d2:20:7b:c6:52:e3:f0:2b:ea:1f:62:a2:c9:1a:8f:
         7d:86:1c:21:7e:42:01:5a:1b:d2:a3:4b:eb:e4:81:9e:b2

そして最後の証明書です。これは Issuer も Subject も vc.vrs.digital.go.jp Root CA となっており、いわゆる「自己署名証明書」となっています。そして、この証明書が信頼に足るものであれば、連鎖的にそれぞれが持つ公開鍵が信頼できるものであるとすることができるという仕組みです。 (これ実際はどうやってルート証明書として配るんだろう、、、?アプリに入れる??)

証明書チェーンの検証は openssl を使用して以下のコマンドで実行できます。

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

以上、JWK からわかる証明書チェーンの話でした。まとめるとこんな感じ。

3番目の証明書 2番目の証明書 1番目の証明書 ワクチン接種証明書
Issuer vc.vrs.digital.go.jp Root CA vc.vrs.digital.go.jp Root CA vc.vrs.digital.go.jp CA vc.vrs.digital.go.jp Issuer
Subject vc.vrs.digital.go.jp Root CA vc.vrs.digital.go.jp CA vc.vrs.digital.go.jp Issuer わたし
内容 Subjectの公開鍵 Subjectの公開鍵 Subjectの公開鍵 Subjectの接種記録

(・・・JWS の署名検証まで openssl の dgst などでできるとよかったんですがやり方がわかりませんでした😅)

終わりに

ただの「やってみた」程度で書き散らかしてしまいましたが、ここら辺の内容を、詳しく知りたい方は以下の記事をオススメいたします。

zenn.dev

ソースコード

パースから署名検証までを行うコードをGistに公開しました。

Parse and verify SMART Health Cards. · GitHub