いしぐめも

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

新型コロナワクチン接種証明書の署名を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

新型コロナワクチン接種証明書アプリで発行された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コード向けの容量削減テクニックらしく、そういうのも楽しいなと思いました。

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

プロフィールムービー用に顔の正規化を行うスクリプトを書きました

PythonOpenCVを使って、顔写真の目と口の位置を合わせる処理「正規化」のスクリプトを組んでみました。

顔写真は撮った時のタイミングによって角度だったり、人物だったりが異なるわけですが、それを両目の位置、口の両端を手動で選択することで一定の位置にそろえることができるというものです。

はじめに

私事ですが、最近結婚式を挙げました。披露宴ではド定番の演出として新郎新婦が中座するときにお客さんに見てもらう「プロフィールムービー」というものがありますが、画像処理を齧った身としては凝った演出をしてみたくなるものです。というわけで今回は顔の正規化にチャレンジしてみました。

正規化とは

機械学習でGANとかを勉強し始めて「正規化」というものがあることを知りました。所定の位置に両目と口の位置を揃える処理ですが、どの正規化を適用するかなどは論文によって異なるのかもしれません。

以下に、いわゆる PGGAN の論文「Progressive Growing of GANs for Improved Quality, Stability, and Variation1」から正規化部分の画像を引用します。

f:id:yoh1496:20211103000018p:plain
ICLR 2018 PGGAN Figure 8 より引用

おそらく学習をさせる際に少しでもパラメータを減らす効果があるんじゃないかなと思います。が、それはある意味、そこに目がある口があるという前提になるので下記みたいに顔でないものに適用すると顔が出てくることになります。シャポコさんのツイートが例として面白かったので引用させていただきます(問題あれば言ってください)

正規化の手順

PGGANで使用している正規化手順は以下の通り。

  1. 左目から右目を結ぶベクトル x'
  2. 両目の中点から口の中点を結ぶベクトル y'
  3. 両目の中点から口の中点に向けて 0.1 |y'| を進めた位置を中点 c
  4. 切り出す幅・高さは 4.0 * 両目間の距離, 3.6 * 口と目の間の距離 のどちらかを使用する
  5. x' と平行な単位ベクトルと、それに垂直な単位ベクトルを新たなx軸, y軸とする

こんな感じでしょうか。これをスクリプトに起こしました。

正規化スクリプトの使い方

上記の正規化をポチポチGUIで実行できるスクリプトにしました。プルリク大歓迎です。

github.com

input画像の準備

index.py と同じ階層に input というフォルダを作り、画像ファイルを放り込んでください。サブフォルダがあっても大丈夫です。

f:id:yoh1496:20211103011844p:plain
インプット例(画像はpixabayから)

以下に使用した素材へのリンクを貼っておきます。

スクリプト実行

実行するにはリポジトリ内の index.py を実行してください。

python index.py

f:id:yoh1496:20211103012658p:plain
WinPythonでもopencvを導入すれば実行できます

点選択

まずはインプット画像が表示されます。

f:id:yoh1496:20211103013040p:plain
インプット画像が表示される

画像上の目と口の位置をクリックして決めていきます。順序は「画像向かって左の目 → 画像向かって右の目 → 口の左端 → 口の右端」です。

ただし、このときフィードバックないので注意してください。選び終わった点ぐらい表示すればよかったかな…(プルリク大歓迎です)

f:id:yoh1496:20211103013219p:plain
正規化後

4点決めた時点で正規化された画像が表示されます。問題なければエンターキーを押して次にすすんでください。

画像が一巡するまでこれを繰り返したら終了です。なおアンドゥなどという高度な機能は実装していないのでミスったら最初からやり直してください。(プルリク大歓迎です)

結果

結果は output フォルダに作成されます。

f:id:yoh1496:20211103013617p:plain
結果

こんな感じに、すべての顔が画像中心に来ていれば成功です。

出力時の画像サイズ、入力時の表示サイズはパラメータで変更できるようにしてあるので、適宜いじって使用してください。(拡大縮小機能????プルリク大歓迎です)

これが正規化パワーだ!

f:id:yoh1496:20211103014058g:plain
正規化結果アニメーション

というわけで、こんな感じにいろんな角度・向きで取られた顔写真を正規化することで、たとえそれが別人であっても顔が重なるように処理することができました。

このツールを作ったわけ

こんな正規化ツールを作って何がしたかったかのかは、冒頭にも記載した通りプロフィールムービーの作成です。

実家にあった子供の頃の写真と今までの写真を全部正規化して時系列に並べることで成長の過程をアニメーションで見せたく、また、子供の頃の写真ってほかの友達と映っていたり真正面を向いていなかったりで顔の識別だったり顔のランドマーク検出がうまくいかないだろうなーということが予見されたので、甘んじて手動でポチポチやるツールになりました。

結果、数百枚あった自分の写真をポチポチ正規化すると、「(昔はかわいかったのに)どうしてこうなった!」というおぞましいアニメーションができあがりました。

尺の関係と、おそらく当日プロジェクターで投影してもよくわからないだろうなという懸念から使用は見送りましたが、自分のプロフィールムービーで使ってやるぜ!という方がいらしたら是非使ってやってください。

終わりに

以上、顔の正規化スクリプトの話でした。

OpenCVのマウスコールバックなんて18095710978万年ぶりに使ったので色々と懐かしく、楽しいプログラミングでした。


  1. Tero Karras, Timo Aila, Samuli Laine, Jaakko Lehtinen: “Progressive Growing of GANs for Improved Quality, Stability, and Variation”, 2017; arXiv:1710.10196.

はてなブログの記事にJSON-LDで構造化データを入れる

最近JSON-LDの勉強を始めて、どこかで実践できる場はないかな?ということで、自分のブログの記事ページに対して、JSON-LDで記述した情報を記載してみることにしました。

この内容は既に実践されている方がいて、以下の記事を大変参考にさせていただきました。

cartman0.hatenablog.com

リッチリザルトテスト

構造化データが正しくセットされているかどうかは以下の「リッチリザルトテスト」を行うことで確認できます。

Rich Results Test - Google Search Console

具体的には、指定したURLに対してGoogleクローラーが走り、必須項目などが正しくセットされているかを確認することができます。

記事ページにデフォルトで含まれるJSON-LD

とりあえずデフォルトでリッチリザルトテストを行ってみました。

f:id:yoh1496:20211028113703p:plain
デフォルトの記事ページに対してリッチリザルトテストを行った結果

すると、なんとデフォルトでもJSON-LDによる構造化データが含まれていることがわかりました。しかし以下のように警告が出てくるので見てみると、、、

f:id:yoh1496:20211028114154p:plain
デフォルトではauthorが指定されていないという警告

<script type="application/ld+json">
{
  "@context":"http://schema.org",
  "@type":"Article",
  "dateModified":"2021-09-13T18:32:07+09:00",
  "datePublished":"2021-09-10T20:47:53+09:00",
  "headline":"Personium上のファイルを操作(ダウンロード・アップロード・アクセス制御)してみよう",
  "image":["https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoh1496/20210910/20210910200847.png"]
}
</script>

実際の記述がこちら(インデントは著者によるもの)。確かに、authorが含まれていません。

追加情報を記述するスクリプトを配置する

というわけで、基本的なデータははてなブログにデフォで記述されているものを踏襲するとして、足りていないauthorなどのデータを cartmanさんの記事 を参考に入れていきます。

スクリプト

cartmanさんの記事からちょっと修正し、すでにJSON-LDが含まれている場合はそれを上書きするようにしました。利用する際は AUTHOR_ADDRESS AUTHOR_NAME AUTHOR_URL を適宜変更してください。

<!-- Article JSON-LD -->
<script type="text/javascript">
  (function() {
    /* personal settings */
    var AUTHOR_ADDRESS = "Japan";
    var AUTHOR_NAME = "yoh1496";
    var AUTHOR_URL = "https://github.com/yoh1496";

    function create_schemaorg_article() {
      function tr(f) { try { return f.call(); } catch (e) { } }

      /* base json */
      var ae = document.querySelector('script[type="application/ld+json"]');
      if (!ae) return;

      var article_obj = Object.assign({
        "@context": "http://schema.org",
        "@type": "Article",
        "fileFormat": "text/html",
        "isAccessibleForFree": true,
      }, JSON.parse(ae.innerText));

      /* setting */
      var name = undefined || tr(function() { return document.querySelector("h1.entry-title").innerText; });
      var headline = undefined || tr(function() { return name.substr(0, 109); }); // [0, 110]まで
      var uri = undefined ||
        tr(function() { return document.querySelector(".entry-title-link").getAttribute("href"); }) ||
        tr(function() { return document.querySelector('[property="og:url"]').getAttribute("content"); });
      var image = undefined || tr(function() { return document.querySelector('[itemprop="image"]').getAttribute("content"); });
      var description = undefined || tr(function() { return document.querySelector('[name="description"]').getAttribute("content"); });
      var datePublished = undefined || tr(function() { return document.querySelector('[pubdate]').getAttribute("datetime"); });
      var dateModified = undefined || tr(function() { return document.querySelector("time[itemprop]").getAttribute("datetime"); });
      var person_image = undefined || tr(function() { return document.querySelector('.profile-icon').getAttribute("src"); });

      var person = {
        "@type": "Person",
        "address": AUTHOR_ADDRESS,
        "name": AUTHOR_NAME,
        "url": AUTHOR_URL,
      };
      if (person_image) person["image"] = person_image;

      var publisher_name = undefined || tr(function() { return document.querySelector("[data-blog-name]").getAttribute("data-blog-name"); });
      var publisher_url = undefined || tr(function() { return document.querySelector("[data-blog-uri]").getAttribute("data-blog-uri"); });
      var publisher_logo_image_url = "https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png";
      var publisher = {
        "@type": "Organization"
      };
      if (publisher_name) publisher["name"] = publisher_name;
      if (publisher_url) publisher["url"] = publisher_url;
      if (publisher_logo_image_url) publisher["logo"] = {
        "@type": "ImageObject",
        "url": publisher_logo_image_url
      };

      var keywords = undefined || tr(function() {
        var arr = [];
        document.querySelectorAll(".entry-category-link").forEach(function(item) {
          arr.push(item.innerText);
        });
        return arr;
      });
      var genre = keywords;
      var charset = undefined || tr(function() { return document.querySelector('[charset]').getAttribute("charset"); });
      var copyrightYear = undefined || tr(function() { return datePublished.match(/^(\d{4})-/)[1]; });
      var inLanguage = undefined || tr(function() { return document.querySelector('[data-avail-langs]').getAttribute("data-avail-langs").split(" "); });

      // create article_json
      if (name) article_obj["name"] = name;
      if (headline) article_obj["headline"] = headline;
      if (uri) article_obj["url"] = uri;
      if (uri) article_obj["mainEntityOfPage"] = { "@type": "WebPage", "@id": uri };
      if (image) article_obj["image"] = image;
      if (image) article_obj["thumbnailUrl"] = image;
      if (description) article_obj["description"] = description;
      if (keywords) article_obj["keywords"] = keywords;
      if (charset) article_obj["encoding"] = { "@type": "MediaObject", "encodingFormat": charset };
      if (person) article_obj["author"] = person;
      if (publisher) article_obj["publisher"] = publisher;
      if (person) article_obj["copyrightHolder"] = person;
      if (copyrightYear) article_obj["copyrightYear"] = copyrightYear;
      if (datePublished) article_obj["datePublished"] = datePublished;
      if (dateModified) article_obj["dateModified"] = dateModified;
      if (inLanguage) article_obj["inLanguage"] = inLanguage;
      if (genre) article_obj["genre"] = genre;

      ae.innerText = JSON.stringify(article_obj);
    }
    window.addEventListener("load", create_schemaorg_article, false)
  }());
</script>

設定方法

上記スクリプトを配置する場所は 「設定」 - 「詳細設定」 とたどっていき…

f:id:yoh1496:20211028133406p:plain
ダッシュボードからの「設定」「詳細設定」をクリック

「要素にメタデータを追加」 のテキストエリアに上記スクリプトをコピペします。

f:id:yoh1496:20211028133543p:plain
<head> 要素にメタデータを追加

コピペしたら「保存する」ボタンで設定を適用してください。

確認方法

最初と同じようにリッチリザルトテストで結果を確認します。

f:id:yoh1496:20211028133908p:plain
警告が解消された!

終わりに

今回はブログにスクリプトを配置して構造化データを埋め込むということをやってみました。

スクリプトを書きながら「これじゃIEで動かないな~~」とか「スマホでも有効な配置場所にしなきゃなー」とか考えたんですけど、 クローラーに読ませる分には関係ないなと今更ながら思いました。(一応この手順たどればとりあえずリッチリザルトテストではモバイル版でも通るようになってます)

リンクトデータ自体はあまり身近にあるような感じがしなくてピンと来てなかったんですが、身の回りの情報に少しずつつけていくことでちょっと身近に感じられるようになる気がしました。

Personium上のファイルを操作(ダウンロード・アップロード・アクセス制御)してみよう

本記事では、PersoniumのBox上のファイルを操作する方法をコードサンプルと共に書いてみます。

CLI

PersoniumのBoxは、WebDAVという、HTTPを拡張した仕様によってファイルの読み書きを行うことができます。

WebDAV」という言葉自体、個人的には馴染みがなかったんですが、ファイルストレージ系クラウドサービスで使用されていたり、実は世の中に浸透しているのかもしれません。HTTP/HTTPSで読み書きが行われるので、HTTPプロキシをそのまま流用できたり、というのが便利だったりするのかな?と思います。

PersoniumもWebDAVという共通仕様を使用することができるので、世の中のライブラリを使用することもできます。が、今回は例のためにHTTP通信のAPIを使用して使ってみるというのを示してみたいと思います。

ファイル一覧の取得(PROPFIND)

PROPFIND メソッドは指定されたURIに対応するリソースの属性を取得するのに使用します。ファイルに対して行えば、ファイルの属性を取得できますし、コレクション(≒ディレクトリ)に行えば、それに含まれるリソース一覧と属性を取得することができます。

ファイルのアップロード(PUT)

ファイルの取得(GET)

アクセスコントロールの設定(ACL

Personiumのキモとも言える、アクセスコントロール設定のやり方です。ここはWebDAVの仕様に基づいてXMLを組み立てる必要があります(すごくつらい)




GUI

Personiumでは、セルを管理するWebアプリケーションを公開しています。このツールを利用することで上記で説明したようなファイルのアップロード・アクセスコントロールAPIの実行などを画面を通して行うことができます。

github.com

本来、このツールを動作させるためには、Personiumのセル上に配備される想定ですが、ただの静的なWebアプリなのでフォークしてGitHub Pagesにて公開しました。こちらを利用しても同じことができます↓

Personium Unit Manager

ログイン

上記リンクをクリックすると、ログイン画面が開きます。

f:id:yoh1496:20210910200847p:plain
ログイン画面

ここに以下のような情報を入力して、ログインします。管理者からセルの払い出しを受けた際にもらった情報を入力してください。

# 項目 説明
1 セルURL ログインする先のセルURL (例:https://cell.pds.example.com/
2 アカウント名 セル内のアカウント名 (例:account)
3 パスワード セル内のアカウントパスワード (例:XjEd6Ucr)

※ここで権限の弱いロールを付与されたアカウントでログインすると以降で説明する操作ができない可能性があります

ボックス作成・選択

ログインすると最初にエクスプローラーのような画面が開き、ボックス一覧が表示されます。デフォルトではセルのメインボックスが [main] という名前で一覧に表示されます。

この画面では、ボックス名をクリックしてボックスの中身を見たり、「Create Box」ボタンから新しいボックスを作成したりすることができます。

f:id:yoh1496:20210910201909p:plainf:id:yoh1496:20210910202220p:plain
ボックス一覧・ボックス作成

ボックス作成ダイアログには「URL to identify the schema of this application」と書かれたテキスト入力がありますが、そこはクライアント認証(特定のクライアント以外に読み書きを認めない機能)を利用するためのURL入力箇所ですので、空白のままでも大丈夫です。

ボックスへファイルのアップロード

ボックスを開くと、内容が表示されます(デフォルトは空です)。そこで「Upload」ボタンをクリックするとファイル選択画面が開きますので、そこでファイルを選択すると任意のファイルをアップロードすることができます。

f:id:yoh1496:20210910202819p:plainf:id:yoh1496:20210910203119p:plain
ファイルのアップロード・アップロードされたファイルのURL確認

アップロードされたファイル、名前の横のチェックボックスをクリックすることでファイル情報を確認することができ、外部からアクセスする際に使えるURLを確認することができます。URLは <セルURL><ボックス名>/<ファイル名> という形式になっているはずです。

ファイルのアクセスコントロール設定

ファイルのアクセスコントロール設定を変更するには、操作したいファイルのチェックボックスにチェックを入れた状態で、「ACL Settings」横の編集ボタンをクリックします。

f:id:yoh1496:20210910203454p:plain
ACL Settings横の鉛筆マークをクリック

ここで「付与する対象」と「付与する権限」を確認し、チェックマークを入れ「Save」を押します。(例では anyoneread を与えてみます。注意:この設定を行うと全ユーザー(未認証含む)に対して公開されます

f:id:yoh1496:20210910203830p:plain
権限の付与

そしてファイルのURLにアクセスしてみると、、、以下のようにアップロードしたコンテンツが表示されます。

f:id:yoh1496:20210910204232p:plain
アップロード・アクセスコントロール設定したファイルが表示される

終わりに

パーソナルデータストアなのに、ファイルをどうやってアップロードするか?といった内容の記事が全然検索でヒットせず、絶望したので記事を書いてみました。Personiumのボックスは、Webサイトの静的コンテンツを配信するのにも使用可能なので、Webアプリ公開にも使用できます。Webアプリ配信元のセルURLを使用して、クライアント認証を行う「Personiumアプリ」というものもありますが、それについてはまた後日記事にしたいと思います。

感想・質問・改善提案、なんでもウェルカムですので、コメントお願いいたします!

Personium の access token をデコードして理解する

この記事では、Personiumの動作の理解を深めるためにアクセストークンについて調査を行った内容を記載しています。本調査を行うことで、Personiumを運用する上での理解を深めていきたいと思います。

なお、本記事の記載内容は、2021年9月現在GitHubで公開されているソースコードから読み解ける内容を記載したものであり、一切の秘密情報を含みません。

トークン?

基本的に、PersoniumはAPIリクエストを行ったユーザーを識別するのにHTTPのAuthorization ヘッダに指定された「トークン(アクセストークン)」を用いています。ユーザーの識別に使用できるということは、アクセストークンは簡単に外部の人が偽造できないような仕組みになっている必要があるのですが、Personiumはどのようにそれを行っているのでしょうか?

今回読むソースコード

Personiumのアクセストークン生成まわりのコードは personium-lib-commonio.personium.common.auth.token あたりに記載されていそうです。

personium-lib-common/src/main/java/io/personium/common/auth/token at develop · personium/personium-lib-common · GitHub

アクセストークンの種類

用語集 · Personium

Personiumのドキュメントによれば、トークンには「セルローカルトークン」 と、「トランスセルトークン」 という2種類が存在します。

「セルローカルトークン」とは、名前の通り、ローカルに使用可能なトークンです。自分のセルに対して、「私はこのセルのアカウントAだ」というのを示すのに使用できるトークンです。

一方、「トランスセルトークン」は、セルをまたいで「隣のセルのものですが・・・」という感じで自分のセル以外に対して自身の身元を証明するために使用するトークンです。

今回の記事ではこの辺の違いをソースコードを読み解いていき、各トークンが示す内容などを明らかにしていければな、と考えています。

セルローカルトーク

まずは「セルローカルトークン」です。セルローカルトークンはResidentLocalAccessToken クラスとして実装されており、文字列としてのトークン化が実装されているのは、継承元のAbstractLocalTokenです。

personium-lib-common/AbstractLocalToken.java at develop · personium/personium-lib-common · GitHub

アクセストークンの生成

では、さっそくアクセストークンを見てみましょう。

String issuer = "https://cell.pds.example.com/";
ResidentLocalAccessToken.setKeyString("hogehogefugafuga");
ResidentLocalAccessToken token = new ResidentLocalAccessToken(1000, 3600, issuer, "me", "schema", new String[] { "scopeA", "scopeB" });
System.out.println(token.toTokenString());

こんな感じでセルローカルトークンを生成してみます。

AR~6LIBaVj_ICPpQvu2lt8M3Q0weZ0DayiQilJNkb4-220DBR4h9S5ky6Smu1op3XwZG62w3s4N--vDLViGFyQZqRgJ9r4m9Bs0rBIlTuZaXjw

それっぽい文字列が出てきました。この文字は以下のような加工をされたBase64の文字列です。

  • AR~ という文字列を先頭に付ける
  • +- に置換する
  • \_に置換する
  • 末尾の = を取り除く

というわけで、これを逆に変換してみます

6LIBaVj/ICPpQvu2lt8M3Q0weZ0DayiQilJNkb4+220DBR4h9S5ky6Smu1op3XwZG62w3s4N++vDLViGFyQZqRgJ9r4m9Bs0rBIlTuZaXjw=

最後の= の数は文字の長さが4の倍数になるように調整してください。これを Base64 でデコードすればトークン情報のバイト列になります。この「トークン情報のバイト列」ですが、 AbstractLocalToken#doCreateTokenString で生成したトークン情報文字列を128bit長の鍵を使用したAES方式による暗号化が施されたものです。

アクセストークンの復号化

では、復号化してみましょう。opensslコマンドに -base64 オプションを付けて、Base64デコードしながら復号化してみます。

echo '6LIBaVj/ICPpQvu2lt8M3Q0weZ0DayiQilJNkb4+220DBR4h9S5ky6Smu1op3XwZG62w3s4N++vDLViGFyQZqRgJ9r4m9Bs0rBIlTuZaXjw=' | openssl aes-128-cbc -d -base64 -K '686F6765686F67656675676166756761' -iv 'ae03f002bb60aa0ce591f214984c6d23'

出力↓

0001    0       3600    me      schema  scopeA scopeB   https://cell.pds.example.com/

と、こんな感じでトークン情報が出てきます。ここで使用した K とか iv とかのパラメータは後述します。

アクセストークンに入っている情報

というわけで、まとめるとこんな感じです。以下の情報がタブ(\t)区切りで入ってます。

# 情報
0 生成日時(エポックミリ秒)
1 種類
2 有効期間(ミリ秒)
3 アカウント名
4 認証クライアント情報( 参照: アプリ認証 · Personium
5 スコープ
6 発行元(セル)

アクセストークンの復号化に必要なパラメータ

openssl コマンドで使用した K とか iv に使用したパラメータですが、それぞれ以下のように作っています。

K(AES 128bit暗号鍵)

printf 'hogehogefugafuga' | hexdump -e '16/1 "%X"'

iv(AES 初期化ベクトル)

printf 'https://cell.pds.example.com/' | md5sum

K の生成に使用したのは hogehogefugafuga という16バイトの文字列です。これはアクセストークン生成時の setKeyString 関数で指定したものですが、Personiumではデフォルトでユニットの設定 io.personium.core.security.secret16 で指定されたものが使用されます。iv は発行元セルのURLをmd5ハッシュ値です。

トランスセルトーク

トランスセルトークンとは、自身のセルでしか使えない(復号化できない)セルローカルトークンに対し、他のセルでも使えるように定められた全く別の種類のトークンです。

https://personium.io/docs/ja/server-operator/unit_operation_design/#personium%E3%81%A8pki

PersoniumでCellをまたがったやり取りに用いられているTrans-Cell AccessTokenはSAML2.0のAssertion形式となっており、Assertionで用いる電子署名としてPKIの代表的な仕様であるx.509を使用したものを採用しています

なるほどわからん

とりえあず、ソースコードを読むとSAML Assertionをbase64エンコードしたものがトランスセルトークンらしいので、base64 -d でデコードしてみることにします。

トランスセルトークンの取得

トランスセルトークンは通常のアクセストークン取得のボディに p_target として宛先セルURLを指定すると発行することができます。

curl https://cell1.pds.example.com/__token -d 'grant_type=password&username=USERNAME&password=PASSWORD&p_target=https://cell2.pds.example.com/' -X POST | jq .access_token --raw-output

上記コマンドを実行して得られるトークンは「持ち主はcell1の○○さん」ということを示す名札のようなものです。文字数は5000文字近くありました。

トランスセルトークンのデコード

base64エンコードされているので、デコードしてみます。 $TRANSCELL_TOKEN は上記コマンドで取得したアクセストークンで読み替えてください。

echo $TRANSCELL_TOKEN | sed -e 's/-/+/g' -e 's/_/\\/g' | base64 -d

このコマンドで実施しているのは 「-を+に置換、_を\に置換」と「base64デコード」の2つの操作です。そして出力されたのがコチラ↓(成形は筆者によるもの)

<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="aa9cfc1f-8a86-4e57-a64c-b3e405de97e7" IssueInstant="2021-10-04T06:21:29.639Z" Version="2.0">
  <Issuer>https://cell1.pds.example.com/</Issuer>
  <Subject>
    <NameID>https://cell1.pds.example.com/#username</NameID>
    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
      <SubjectConfirmationData NotOnOrAfter="2021-10-04T07:21:29.639Z" Recipient="https://cell2.pds.example.com/__token" />
    </SubjectConfirmation>
  </Subject>
  <Conditions>
    <AudienceRestriction>
      <Audience>https://cell2.pds.example.com/</Audience>
      <Audience />
    </AudienceRestriction>
  </Conditions>
  <AuthnStatement AuthnInstant="2021-10-04T06:21:29.639Z">
    <AuthnContext>
      <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
    </AuthnContext>
  </AuthnStatement>
  <AttributeStatement>
    <Attribute Name="Roles" NameFormat="urn:x-personium:xmlns">
      <AttributeValue>https://cell1.pds.example.com/__role/__/admin</AttributeValue>
    </Attribute>
    <Attribute Name="Scopes">
      <AttributeValue>root</AttributeValue>
    </Attribute>
  </AttributeStatement>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>DIGEST_VALUE</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>SIGNATURE_VALUE</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509SubjectName>CN=pds.example.com,O=Internet Widgits Pty Ltd,ST=Some-State,C=AU</X509SubjectName>
        <X509Certificate>X509_CERT</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>
</Assertion>

もろ SAML Assertionですね。セルローカルなアクセストークン(タブ区切り)とは打って変わって、ガッツリXMLが入っていました。

トランスセルトークンの検証

Personiumにおいて、トランスセルトークンに記載されている「cell1セルの○○さん」という情報は、XML署名の手法に基づいて ユニット(pds.example.com)が署名 します。トランスセルトークンの SAML Assertion の KeyInfo には X509の証明書が含まれています。

これを検証することで、このトークンは確かに pds.example.comユニットによって発行されたもの なんだなと受け取った側は知ることができます。

検証プロセスについては後で調べます・・・

終わりに

学習のためにトークンを手動でパースしてみました。トークンに含まれている情報や生成に必要な秘密情報を知っておくことで、Personiumがどのようにデータへのアクセスコントロールを行っているのか、また、どういったパラメータが漏洩するとマズいのか、理解を深める一助になれば幸いです。

Personiumでよく使う用語集

Personiumでよく使う用語

Personiumの用語、Personium自体が汎用的な作りになっていることもあり、かなり含みを持たせたような説明になっているので例とかを交えて紹介してみます。 理解への一助になれば幸いです。

適宜更新します

セル(Cell)

Cellは、データ主体ごとのData Storeです。個人で使う場合はPDS(Personal Data Store)となります。

https://personium.io/docs/ja/introduction/001_Personium_Architecture/#cell

いきなり「データ主体」という言葉が出てきました。「データ主体」というのは、データの持ち主・管理者を表す概念ですが、ここであえて「データ主体」という単語を使っているのは、Personiumをどのように使うかによってデータ主体は異なってくるためです。

例えば、1つ1つのセルを各企業に割り振り、各企業のデータ格納スペースとして使用する場合、そのセルのデータ主体は「企業」となります。1人1人にセルを割り与え、個人データを格納してもらうようにするのであれば、データ主体は「個人」です。

このように、Personiumは「セル」という単位でデータ主体(ヒト・モノ・組織、など)がデータの管理を行える領域を提供します。

ボックス(Box)

Boxは、アプリケーション用のデータストアです。

https://personium.io/docs/ja/introduction/001_Personium_Architecture/#box

「アプリケーション」と出てきましたが、要はセルを「用途」ごとに分けたディレクトリ(フォルダ)だと思ってください。

普通に使う分にはただのディレクトリですが、アプリケーションと紐づけて利用することで、そのアプリ専用のディレクトリとして振舞わせることができます。

Androidで言えば、/Android/data/ フォルダのようなもので、当該アプリからは書き込めるけど他のアプリからは読み書きできないといった感じです。