新型コロナワクチン接種証明書の署名をpythonで検証する
SMART Health Cards (SHC) はJSON Web Signature (JSON Web Signature) という形式で表されてからQRコード化されるため、そのQRコードを読み取った人が「接種証明書に記載されている発行元が発行したものか」「発行されてから改ざんされていないか」を検証できる「署名」を含みます。 今回はその署名データをpythonを用いて検証してみたいと思います。
SMART Health Cards は検証可能
前回記事では、pythonを使って「新型コロナワクチン接種証明書」のSMART Health Cards をパースしてみたという内容を書きました。検証方法について省略してしまったので、今回はそこらへんのコードを示したいと思います。
パースなど前回の内容はコチラ↓
JWS 概要
SHC で使用されている JWS は「Compact Serialization」と呼ばれる方式で表現された JWS であり、以下の3パートを base64url でエンコードし、 .(ドット)で連結した 文字列です。
概要としてはこんな感じで、詳細は以下の RFC7515 を参照してください。
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」で定義されているので、詳細はそちらを参照してください。
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として読んでもよい)
こんな感じ↓
それを 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 が示している」ということです。
そしてこの証明書が正しいことを確認するためには署名を検証する必要があるのですが、それにはこの 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 などでできるとよかったんですがやり方がわかりませんでした😅)
終わりに
ただの「やってみた」程度で書き散らかしてしまいましたが、ここら辺の内容を、詳しく知りたい方は以下の記事をオススメいたします。
ソースコード
パースから署名検証までを行うコードをGistに公開しました。