CBOR/COSEベースのEUワクチン接種証明書をPythonで覗いてみる

2021.12.23

日本で新型コロナワクチン接種証明書の運用が始まりました。

W3C Verifiable CredentialsベースのSMART Health Cards形式が採用され、解読を楽しんでいる人を見かけるようになりました。

ブースターを打って証明書も新しくなり、普段使っているEUのワクチンパスポート(EU Digital COVID Certificate)はどうなっているのか気になったため、Pythonで解読してみました。

EUのワクチンパスポート運用

EUでは、ワクチンの集団接種会場や薬局でワクチン接種を証明するQRコードが発行されます。

専用のアプリ、例えば、ドイツであれば CovPass、フランスであれば、pass sanitaire(衛生パス)などで読み取り、フライト搭乗時、イベント、外食などの際に利用します。

証明書にはワクチンの接種状況だけでなく、接種者の氏名・生年月日も含まれています。

実際のワクチンパスポートの運用では、何もチェックしなかったり、それっぽいQRコードを目視で済ますこともあれば、QRコードをスキャンし、フォトIDで本人確認することもあります。

EU圏を散策するときは、EU規格の証明書を取得し、フォトIDを常に持ち歩きましょう。

QR コードの仕様

QRコードには

  • 名前
  • 誕生日
  • ワクチン接種日
  • ワクチンの種類

などの情報が含まれています。

QRの2Dコードにコンパクトに収まるように、JSONのバイナリ版とも言えるConcise Binary Object Representation (CBOR)が採用され、データの完全性のために、CBOR Object Signing and Encryption(COSE) が採用されています。

To optimize the footprint of the 2D Code, the objects are encoded as CBOR1 object. To ensure the data integrity, the CBOR Object Signature and Encryption is used.

https://ec.europa.eu/health/sites/default/files/ehealth/docs/digital-green-certificates_v3_en.pdf

で標準化されています。

具体的には、下図のように、JSON形式のデータをCBORにエンコードし、Deflate圧縮したものを Base45 エンコードしています。

※図は Technical Specifications for Digital Green Certificates Volume 3のFigure 1: Serialization Process から

CBOR/COSEはパスワードレスなFIDO認証の WebAuthn でご存知のかたが多いかもしれません。

また、Deflate 圧縮されたバイナリデータは QR コード向けに Base45 エンコードが採用されています。

Base45 はバイナリデータを Base64/Base32/Base16に比べてQRフレンドリーに効率よくエンコードできるとされ、IETF ドラフトで標準化が進められています。

Compared to already established Base64, Base32 and Base16 encoding schemes, that are described in RFC 4648, the Base45 scheme described in this document offer a more compact QR- code encoding.

https://datatracker.ietf.org/doc/draft-faltstrom-base45/

Python でQRコードをのぞいてみる

今回は Python を使い、QRコードの情報をデコードしてみます。

証明書生成時のシリアライズを逆順にたどります。

なお、COSE 署名の検証に必要な公開鍵は申請しないと取得できない模様のため、検証処理はスキップしています。

QR コードの読み取り

QRコードの画像を読み取る zbar をインストールします。

$ brew install zbar

QRコードの画像を食わせます。

# test data : https://github.com/eu-digital-green-certificates/dgc-testdata/blob/main/AT/2DCode/raw/1.json
$ zbarimg --raw qr.png
HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-R6IOO6+IUKRG*I.I5BROCWAAT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3GG396B-43O058YIB73A*G3W19UEBY5:PI0EGSP4*2DN43U*0CEBQ/GXQFY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523.K5QZ4A+2XEN QT QTHC31M3+E32R44$28A9H0D3ZCL4JMYAZ+S-A5$XKX6T2YC 35H/ITX8GL2-LH/CJTK96L6SR9MU9RFGJA6Q3QR$P2OIC0JVLA8J3ET3:H3A+2+33U SAAUOT3TPTO4UBZIC0JKQTL*QDKBO.AI9BVYTOCFOPS4IJCOT0$89NT2V457U8+9W2KQ-7LF9-DF07U$B97JJ1D7WKP/HLIJLRKF1MFHJP7NVDEBU1J*Z222E.GJF67Z JA6B.38O4BH*HB0EGLE2%V -3O+J3.PI2G:M1SSP2Y3D38-G9C+Q3OT/.J1CDLKOYUV5C3W1A:75S4LB:6REPKM3ZYO4+QDNDLT2*ESLIH
scanned 1 barcode symbols from 1 images in 0.88 seconds

HC1: で始まる文字列を取得できました。

この先頭の文字列は仕様のバージョンを表し、 HC1Health Certificate Version 1 のことです。

CBOR/Base45 ライブラリをインストール

まずは、デコードに必要なPythonライブラリをインストールします

$ pip3 install -U base45 cbor2

以降はPythonインタープリター上で、シリアライズを逆順にたどり、

  1. Base45
  2. Deflate
  3. CBOR

の順にデコードします。

Base45 デコード

QRコードの先頭4文字(HC1:)は仕様のバージョンを表すため、: よりも後ろの文字列を base45 デコードします。

>>> qr = 'HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-R6IOO6+IUKRG*I.I5BROCWAAT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3GG396B-43O058YIB73A*G3W19UEBY5:PI0EGSP4*2DN43U*0CEBQ/GXQFY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523.K5QZ4A+2XEN QT QTHC31M3+E32R44$28A9H0D3ZCL4JMYAZ+S-A5$XKX6T2YC 35H/ITX8GL2-LH/CJTK96L6SR9MU9RFGJA6Q3QR$P2OIC0JVLA8J3ET3:H3A+2+33U SAAUOT3TPTO4UBZIC0JKQTL*QDKBO.AI9BVYTOCFOPS4IJCOT0$89NT2V457U8+9W2KQ-7LF9-DF07U$B97JJ1D7WKP/HLIJLRKF1MFHJP7NVDEBU1J*Z222E.GJF67Z JA6B.38O4BH*HB0EGLE2%V -3O+J3.PI2G:M1SSP2Y3D38-G9C+Q3OT/.J1CDLKOYUV5C3W1A:75S4LB:6REPKM3ZYO4+QDNDLT2*ESLIH'
>>> import base45
>>> deflate = base45.b45decode(qr[4:])
>>> deflate
b'x\xda\xbb\xd4\xe2\xbb\x88\xc5\xe3\xa6\xa4y\xfc\xc1\xe7\xdb61\xaa-\x88d4^\xc2"\x95p\xd95\x95M*\xe1\xc2\xa2T\xc6$\xc7\x10KF\xe6\x85\x8cK\x12\xcb\x1aW%\xa5\xe41&\xe5&\xe6\xfa\x07\xb9\xeb\x1a\x1a\x18\x18\x18\x1b\x18\x19\x9a&\x95\x15d\x19\x1a\x1aZ\x1a\x9bX\x1a\x18\x98\'\xa5\x94d\x19\x01\x85u\r\x8ct\r-\x92\x92\xf3\x81\x06$%gV\x18\x86\x06\xf9Y\x85\x869{Z\x19\x18Z9\x86X\x19\x1aX\x18\x98[\x98\x18\xbbY\x9a8\xba\xba\x1a\xb8\xba\x9a\x1aX\x1a\xbb99\x1b\x99\x9a8\xb9X\x18\x1a+;%\xe5\x16\xe4\xb8\x86\xea\x1b\xea\x1b\x19\xe8\x1b\x9a\x1aY$e\x16WH\xfbf\xe6e\x16\x97\x14U*\xe4\xa7)x\xa4&\xe6\x94d\xe8(8\x96\x02E2\x13\x93\x8aS\x98\x92J\xd23-L\x0cL\x8d\x81N1K\xceK\xcc]\x92\x9c\x96WR\xea\x1b\x1a\x1c\xe2\x1a\xe4\x16\xe4\x18j\xe3\xee\xef\x1a\x1c\xec\xe9\xe7\xee\x1a\x94\x94\x96W\xea\x0b\xd4\x9aZ\x94V\x94X\xaa\xeb~x\xdb\xe1\xf9\x99y\xe9\xa9E\xc9\xe9y%\x19\xee\x8eNA\x9e\xae>\xaeI\xe9y\x19\xee\x89IE\x99\xa99\xa9\xc9e\xa9E\xa9\x86zFz\x86\xc9)\xf9IY\x86\x96\x96\x16 o\x1a\x99E8|\x9c\xc2X\x1b?\xf9W\xcf\x8den\xc9r\x7f\xfb\xe7\x1f\x14\x9dk\xcd&~\xfc\x0b\xf3\xca\xc9\x1d\xb7?\xdcJ*\xd4\xbe\xe9\xa0\x9d\xd8h\xf2\xc1q\xfa\x9f\x19\xdcOg(\xf4\xa6\xed7=\xc8R\x1b\xad\xc3\xf5\xd4\xb6j\xed\x15\x00\xe0\x19\x89\xb8'

Deflate 伸長

Deflate 圧縮されているため、伸長します。

>>> import zlib
>>> cbor = zlib.decompress(deflate)
>>> cbor
b'\xd2\x84M\xa2\x04H\xd9\x197_\xc1\xe7\xb6\xb2\x01&\xa0Y\x013\xa4\x04\x1a`\xd3Ee\x06\x1a`\xd0\xa2e\x01bAT9\x01\x03\xa1\x01\xa4av\x81\xaabdn\x01bmamORG-100030215bvpj1119349007bdtj2021-02-18bcobATbcix1URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD813#BbmplEU/1/20/1528bisx\x1bMinistry of Health, Austriabsd\x02btgi840539006cnam\xa4cfntuMUSTERFRAU<GOESSINGERbfnuMusterfrau-G\xc3\xb6\xc3\x9fingercgnthGABRIELEbgnhGabrielecvere1.2.1cdobj1998-02-26X@\xf1\x94\x01}_\x93\xfa\x8c\xd8\xa6Fc\x1e\xfd\x8f\x9f\xc1\x15\x9d;\x06\x17\xc7\xf4\x03\xa9\x93\x88\xdb\xf0\xdabq+\xd9@+a\x814\xf0A\x97\xfc\x98\x0b\xe5\x98 \x8df\xbf5\xc1\x04}[,\n\xe5=z\xad\xd4'

CBOR デコード

最後に CBOR デコードします。

>>> import pprint
>>> import cbor2
>>> pprint.pprint(cbor2.loads(cbor2.loads(cbor).value[2]))
{-260: {1: {'dob': '1998-02-26',  # 誕生日
            'nam': {'fn': 'Musterfrau-Gößinger',  # 性(family name)
                    'fnt': 'MUSTERFRAU<GOESSINGER', # 性(Standardised family name)
                    'gn': 'Gabriele', # 名(Given name)
                    'gnt': 'GABRIELE'}, # 名(Standardised given name)
            'v': [{'ci': 'URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD813#B', # 証明書のID(Unique Certificate Identifier: UVCI)
                   'co': 'AT', # ワクチン接種国(Country of Vaccination)
                   'dn': 1, # ワクチン接種済みの回数(Dose Number)
                   'dt': '2021-02-18', # ワクチンの最終接種年月日(Date of Vaccination")
                   'is': 'Ministry of Health, Austria', # 証明書の発行者(Certificate Issuer)
                   'ma': 'ORG-100030215', # ワクチン製造者(Marketing Authorization Holder - if no MAH present, thenmanufacturer)この番号は「Biontech Manufacturing GmbH=ファイザー」
                   'mp': 'EU/1/20/1528', # ワクチン製造ID(vaccine medicinal product)
                   'sd': 2, # 必要なワクチン接種回数(Total Series of Doses)
                   'tg': '840539006', # disease or agent targeted(固定値)
                   'vp': '1119349007'}],  # vaccine or prophylaxis(mRNAの場合は 1119349007)
            'ver': '1.2.1'}},
 1: 'AT',
 4: 1624458597,  # 証明書の有効期限(Unix Time)
 6: 1624285797}  # 証明書の発行日時(Unix Time)

オリジナルの JSON を復元できました。

スキーマの詳細は、"eHealth Network Guidelines on Value Sets for EU Digital COVID Certificates" にまとめられています。

モ→モ→モとModerna3回目の私の場合、3回目の接種( *1)に対応する証明書を確認します。

v のワクチンブロックが次の様になっていました。

            'v': [{'ci': 'URN:UVCI:01DE/A10017089/...',
                   'co': 'DE',
                   'dn': 3, # ワクチン接種済みの回数(Dose Number)
                   'dt': '2021-12-20',  # ワクチンの最終接種年月日
                   'is': 'Robert Koch-Institut',
                   'ma': 'ORG-100031184',  # Moderna Biotech Spain S.L.
                   'mp': 'EU/1/20/1507',
                   'sd': 3, # 必要なワクチン接種回数(Total Series of Doses)
                   'tg': '840539006',
                   'vp': '1119349007'}],
            'ver': '1.3.0'}},
 1: 'DE',
 4: 1671557770, # 証明書の有効期限=発行日時の1年後
 6: 1640021770} # 証明書の発行日時

すでに2回打っていたので、dn/sd がともに2から3に増えました。

また、dt が3回目の接種日に変わっていました。

最後に

EU規格のワクチンパスポート(EU Digital COVID Certificate)は、アメリカ・日本方式のSMART Health Cardベースではなく、CBOR/COSE ベースです。

QRコードをデコードするだけであれば、Pythonで数行書くだけで済みます。

EUのワクチンパスポートは仕様やリファレンス実装がかなりオープンになっているため、CBOR/COSE と戯れてみたい方は、公式のGitHubレポジトリを眺めてみると面白いと思います。

eu-digital-green-certificates · GitHub

それでは。

参考

脚注

  1. ウイルスに感染・回復した場合も、証明書が発行されます。