新型コロナワクチン接種証明書アプリで表示できるQRコードが公開されている規格だったのでverifyできるか確かめてみた。

新型コロナワクチン接種証明書アプリで表示できるQRコードが公開されている国際規格であり、中身がJWSだったので、verifyできるか確かめてみました。
2021.12.20

IoT事業部のやまたつです。

新型コロナワクチン接種証明書アプリで表示できるQRコードが公開されている国際規格であり、中身がJWSだったのでverifyできるか確かめてみました。

概要

本日、2021年12月20日にデジタル庁より公開された新型コロナワクチン接種証明書アプリを登録してみたところ、巨大なQRコードが表示できることに気が付きました。 「署名された情報とかが入ってるんだろうなぁ。しかし、なぜこんなにでかい?」と思い読み取ってみるとshc:/${とても長い数字列}でした。友人にこの話をしてみると、公開されている国際規格であることを教えてもらいました、しかも中身がJWS!それではverifyしてみるしかないな。となりました。

コード全量

以下は簡易実装です。もととなる情報がもっと大きいと対応できなくなるので、気が向いたら修正します。多分3回目の摂取をすると使えなくなるかもです。あとは尋常じゃなく氏名が長い人とかですね。

import jose from "node-jose";
import pako from "pako";
import fetch from "node-fetch";

const code = process.argv[2];

main(code);

async function main(code: string) {
  // reverse it @see https://github.com/smart-on-fhir/health-cards/blob/0acc3ccc0c40de20fc9c75bf9305c8cda080ae1f/generate-examples/src/index.ts#L213-L217
  const jws = split2Char(code.replace("shc:/", ""))
    .map((str) => Number(str) + 45)
    .map((n) => String.fromCharCode(n))
    .join("");

  const jwks: any = await fetchJwks();
  const keystore = await jose.JWK.asKeyStore(jwks);
  const verifier = jose.JWS.createVerify(keystore);
  const verified = await verifier.verify(jws);

  const payload = Buffer.from(pako.inflateRaw(verified.payload)).toString(
    "utf-8"
  );
  console.info(JSON.stringify(JSON.parse(payload), null, 2));
}

function split2Char(code: string): string[] {
  if (!code) {
    return [];
  }
  const head = code.slice(0, 2);
  const tail = code.slice(2);
  return [head, ...split2Char(tail)];
}

async function fetchJwks() {
  const jwksUrl = "https://vc.vrs.digital.go.jp/issuer/.well-known/jwks.json";
  const res = await fetch(jwksUrl);
  const jwks = await res.json();
  return jwks;
}

詳しく見ていこうと思います。

数字列からJWSを取り出す。

  const jws = split2Char(code.replace("shc:/", ""))
    .map((str) => Number(str) + 45)
    .map((n) => String.fromCharCode(n))
    .join("");

数字列からJWSを取り出す処理です。サンプルコードの実装と反対の実装をしただけです。 もっと大きなJSONから生成されたQRコードの文字列の場合、chunkを分割してそれぞれに対してこの処理を行うため、これよりも実装が複雑になります。僕のQRコードはまだまだヒヨッコであったため、上記の実装で足りました。shc:/1/2/みたいにスラッシュがいっぱいある始まり方をしているひとはこのコードでは対応できません。

JWSの公開鍵を手に入れる

規格にも公開鍵が公開されていることが明示されていたので、探していたところrebuild.fmのmiyagawaさんのツイートがヒットしました!🤩 Googleすごい!

node-fetchでしゅっととります

async function fetchJwks() {
  const jwksUrl = "https://vc.vrs.digital.go.jp/issuer/.well-known/jwks.json";
  const res = await fetch(jwksUrl);
  const jwks = await res.json();
  return jwks;
}

verifyする

node-joseを使ってverifyします。JWK.asKeyStore(jwks)でJWKSのエンドポイントから取れたJSONでそのままverifyする書き方を覚えました。学び!

  const jwks: any = await fetchJwks();
  const keystore = await jose.JWK.asKeyStore(jwks);
  const verifier = jose.JWS.createVerify(keystore);
  const verified = await verifier.verify(jws);

中身

アプリの画面で閲覧できる情報がJSONで見れます!やったね!

{
  "iss": 発行者,
  "vc": {
    "credentialSubject": {
      "fhirVersion": "4.0.1",
      "fhirBundle": {
        "resourceType": "Bundle",
        "type": "collection",
        "entry": [
          僕の名前とか生年月日,
          一回目摂取の情報,
          二回目摂取の情報
        ]
      }
    }
  }
}

おわり

verifyできてよかったです!

コードはこれです。

以上、やまたつでした!