Develop with pleasure!

福岡でCloudとかBlockchainとか。

BLS署名を利用した検証可能なwitness encryption

検証可能なwitness encryptionという新しい暗号プリミティブを利用してDLCを構成する新しい提案のペーパーが発表されてたので↓

https://eprint.iacr.org/2022/499.pdf

「II. TECHNICAL OVERVIEW」の内容を見てみた(まだ一部よく分かってない)。

BLS署名

ペーパーではBLS署名を利用したwitness encryptionスキームが提案されているので、まず前提となるBLS署名について。

ペアリング

BLS署名が利用しているペアリングは、同じ素数qを位数とした、2つの加法巡回群 {G_1, G_2} *1と乗法巡回群 {G_T} *2という3つの巡回群について、

 {e:G_1 \times G_2 \rightarrow G_T}

という関係が成立する写像のことを指す。つまり {G_1} {G_2}の各要素を入力として、 {G_T}の要素を出力できることを意味する。

 {G_1, G_2}については楕円曲線を利用するため、入力は {G_1, G_2}に対応する楕円曲線上の2つの点で、有限体 {G_T}の要素を出力することになる。このことから楕円ペアリングとも呼ばれる。

また、この時、 {G_1}楕円曲線の生成元をP、 {G_2}楕円曲線の生成元をQとした場合、任意の数値a, bが与えられた場合に、

 {e(aP, bQ) = e(bP, aQ) = e(P, Q)^{ab}}

が成立することを双線形性と呼ぶ。

BLS署名

ECDSAやSchnorr署名が1つの群、楕円曲線を使ったデジタル署名方式であるのに対し、BLS署名は↑の3つの群の関係を利用したデジタル署名方式になる。

 {G_1}の曲線の生成点をP、 {G_1}の曲線の生成点をQとした場合、BLS署名は以下のように作成される。

  • ランダムな数値 {x \in \mathbb Z_q}を選択し、これを秘密鍵とする。対応する公開鍵は {G_2}上の点Y = xQ
  • 署名対象のメッセージをmとした場合、 {\sigma = xH_1(m)}を計算する。ここで、 {H_1} {G_1}の曲線の要素を出力するハッシュ関数。この {\sigma}が署名で、かつ {G_1}の曲線上の点になる。

※ 署名方式によって公開鍵と署名に使用する際の {G_1, G_2}が逆になる場合もある。

署名の検証は、

  •  {e(\sigma, Q) = e(H_1(m), Y)}が成立するか検証する。

 {e(\sigma, Q)}は、ペアリングの双線型性から {e(xH_{1}(m), Q) = e(H_{1}(m), Q)^{x} = e(H_{1}(m), xQ) = e(H_{1}(m), Y)}と展開できる。右辺と左辺のペアリング関数の出力結果である有限体 {G_T}の要素が同じであれば検証をパスする。

つまりBLS署名はペアリングの双線形写像の特性を利用して秘密鍵の知識を証明する署名方式であることが分かる。また、ECDSAやSchnorr署名と違って署名の作成に乱数(nonce)を必要としない、決定論的な署名方式。

BLSベースのwitness encryption

検証可能なwitness encryptionに行く前に、前提となるBLSベースのwitness encryptionについて。BLSベースのwitness encryptionというのは、BLS署名を利用した暗号化スキームで、ある公開鍵とメッセージに対応するBLS署名が公開されたら、その署名を使って暗号化されたデータを復号できるというもの。

表記については以下を前提とする。

  • 暗号化対象のメッセージをmとする。
  • 署名に利用する公開鍵の鍵ペアをV = vQとする。vが秘密鍵で、Qは↑の {G_2}楕円曲線の生成点。
  • 署名対象のメッセージを {\tilde{m}}とする(これは暗号化対象のメッセージではない)。
  •  {H_1} {G_1}の要素(点)を出力するハッシュ関数
  •  {H_T} {G_T}の要素のハッシュ値を計算するハッシュ関数

公開鍵Vとメッセージ {\tilde{m}}に対して有効なBLS署名が提供されたら、メッセージmを復号できる暗号スキーム。

暗号化

以下の手順でメッセージmを暗号化する。

  1. ランダムな値 {r_1 \in \mathbb Z_q}を選択する。
  2. ランダムな値 {r_2 \in G_T}を選択する。
  3.  {c_1 = r_1P}とする。
  4.  {h = H_T(r_2)}とする。
  5.  {c_2 = e(V, H_1(\tilde{m}))^{r_1} \cdot r_2}とする。
  6.  {c_3 = h + m}とする。
  7. {c = (c_1, c_2, c_3)}を暗号文とする。

復号化

上記の暗号文は、Vとメッセージ {\tilde{m}}に対するBLS署名 {\tilde{\sigma}(= vH_1(\tilde{m}))}が公開されると、以下の手順で復号できる。

  1. {c = (c_1, c_2, c_3)}をパースする。
  2.  {r = c_2 \cdot e(c_1, \tilde{\sigma})^{-1}}を計算する。
  3.  {h = H_T(r)}を計算する。
  4.  {m = c_3 - h}が復号したメッセージとなる。

↑では、 {\tilde{\sigma} = vH_1(\tilde{m})}であるため、rの計算に出てくる {e(c_1, \tilde{\sigma})^{-1}}は、ペアリング関数の双線形性により、 {e(r_1P, vH_1(\tilde{m}))^{-1} = e(V, H_1(\tilde{m}))^{-r_1}}となる。

そして、 {c_2 = e(V, H_1(\tilde{m}))^{r_1} \cdot r_2}であるため {r = r_2}となり、 {h = H_T(r) = H_T(r_2)}であるため、 {c_3 - h}でメッセージmが入手できると。

復号のためには、h(つまり {r_2})の値が必要になり、それを算出するためには、 {c_2}内のペアリング関数部分を打ち消す必要があり、それをBLSの双線形性と乱数使ってワークするようにしてるの面白いな。

ということで、このスキームを利用すると、(オラクルなどの)ある公開鍵とあるメッセージに対応した署名を入手すると復号可能な暗号文を作成することができる。

条件付きの支払い

DLCが扱うのは、オラクルが証明可能なイベントの結果を条件に支払いを行う条件付き支払いをサポートするユースケース

ここでは、アリスがイベントの結果にコミットし、結果がコミットした値と同じ場合に、ボブに対して支払いをす条件付き支払いを考える。DLCでは、アダプター署名を利用して、署名を完成させるために必要なシークレットを復元するアプローチを採っている。

このような条件付き支払いを成立させるためには、

  • イベントの結果がコミットした内容である場合、ボブは必ず署名を入手することができる。
  • イベントの結果がコミットした内容でない場合、ボブが署名を入手することはできない。

ことを保証する必要がある。さらに追加で、オラクルを分散させて一定数のオラクルによる証明が提供された場合に、署名の入手を可能にする(閾値性)を考慮する必要があるが、今回はシンプルにオラクル1人のケースで考える。

検証可能なwitness encryption

↑の新しいソリューションとして提案されているのが検証可能なwitness encryptionという新しい暗号プリミティブを利用する方法。プロトコルの参加者(アリスとボブ)=ブロックチェーンレイヤーが使用する署名方式はSchnorr署名(もしくはECDSA)で、オラクルの署名方式はBLS署名。まず、前提として、

  • アリスの鍵ペアを {P_A = x_AG}とする。ここで、Gはsecp256k1などの楕円曲線の生成点。
  • ラクルであるオリビアの鍵ペアを {P_O = x_OQ}とする。ここで、Qは上記のペアリングの {G_2}楕円曲線の生成点。
  • アリスがボブに条件付き支払いをするメッセージ(トランザクション)をmとする。

アリスは秘密鍵 {x_A}を使ってメッセージに署名する。通常のSchnorr署名であれば、

  1. ランダムなnonce kを選択
  2. R = kGとする。
  3.  {s = k + H(P_A||R||m)x_A}を計算する。
  4. (R.x, s)が署名

この署名があれば、ボブは資金を手に入れられる。ただ、条件付き支払いなので、アリスは、 {T = tG}を使って、メッセージmに対する事前署名s'を生成する。

  •  {s' = k + t + H(m)x_A}

ボブには、(R, s)の代わりにアダプター署名 {(R, s', T)}を送る。アダプター署名を入手したボブは、tさえ分かれば、有効な署名sを導出できる。

その後は、

  1. アリスはtを暗号化した暗号文cをボブに送信する。この暗号化に↑のwitness encryptionを利用する。
  2. リビアがイベントeをメッセージとして、秘密鍵 {x_O}を使ってBLS署名を作成し、公開する。
  3. ボブはこの署名とcからtを抽出し、s = s' - tをして署名sを手に入れる。

というプロセスを実行するが*3、ここで問題になるのが、ボブがアリスからアダプター署名と暗号文cを受け取った際に、cが暗号化されているので、tが確かに暗号化されているのか検証する術がない点。この検証ができなければ、単にアリスを信頼するしかないということになるのでよろしくない。

そこで、Cut&Chooseという手法を使って、暗号化されたデータが有効なデータ形式であることを検証できるようにしたのが検証可能なwitness encryption。具体的には、アリスが暗号文cを生成する際に、単一のtの暗号文を作るのではなく、以下の手順でλ個の暗号文を作成する(λはセキュリティパラメーター)。

暗号化

t単体を暗号化するのではなく、λ個の乱数 {r_i}(i = 0..λ-1)を生成し、この各 {r_i}を↑のBLSベースのwitness encryptionで暗号化する。そして、このλ個の暗号文とは別に、

  1.  {r_i}について {s_i = r_i + t}を計算する(sym-cipherと呼ばれる)。
  2.  {r_i}について {R_i = r_iG}を計算する。

アリスは、λ個の暗号文と {R_i}とアダプター署名をボブに送信する。

Cut & Choose

その後、

  1. ボブは受け取ったλ個の暗号文と {R_i}からλ/2個のペアを選択して、選択したペアをアリスに伝える。
  2. アリスは、
    • ボブから受け取ったデータに対応する {r_i}の値とwitness encryptionで暗号化に使用した乱数値をボブに送信する。
    • 選択されなかった残りのλ/2個のペアについて、 {s_i}をボブに送信する。
  3. ボブは、
    • 選択したペアについて、 {r_i}をアリスから受け取り、前に受け取った {R_i}について {r_iG = R_i}が成立するか検証する。さらに、オラクルの公開鍵Vとメッセージ {\tilde{m}}およびアリスから受け取った乱数値を使って {r_i}の暗号化計算を行い、それがアリスが送ってきたものと一致するか検証する。これにより、アリスが送ってきた暗号文はすべて {r_i}が暗号化されているだろうことが検証できる。
    • 選択しなかったペアについて、前に受け取った {R_i}とTに対して、 {s_iG = R_i + T}が成立するか検証する。これによりアリスが作成したデータはすべて {s_i = r_i + t}の形式のデータだろうということが検証できる。

そして、オラクルが署名を公開すれば、アリスが開示しなかった {r_i}も復号できるので、 {t = s_i - r_i}を計算して、witness tを入手できるということみたい。

この辺りが、ペーパーの「II. TECHNICAL OVERVIEW」で説明されてるんだけど、これアリスが {r_i}を開示した方のパターンだと、ボブはsym-cipherもらってないからどうやってt計算するんだろう?この辺り、まだよく理解できてない。

ちなみにペーパーの方は、

  • Fiat-Shamir変換した非対話型への変換
  • Cut&Chooseのバッチ化
  • 分散オラクルの閾値

など、実用的な課題に対するプロトコルの拡張が続く。

*1:加法巡回群は、群の元がすべて生成元の整数倍で表現できる群

*2:乗法巡回群は、群の元がすべて生成元の冪乗で表現できる群

*3:イベントの結果に両者が合意していれば、tをそのまま渡して終了

x-only public keyの課題とワークアラウンドな回避策

ちょっと前のBitcoin Optechのニュースレターでx-only public keyの課題とその回避策について取り上げられていたので、詳しく調べてみた。

x-only public keyとは?

Bitcoinに導入されたTaprootでは、署名検証に使用する公開鍵に32バイトのx-only public keyという形式を採用した。

公開鍵はこれまで33バイトの圧縮公開鍵(楕円曲線の点のy座標の偶奇を示す1バイト+x座標32バイト)が使われてきたが、Taprootではy座標は常に偶数となるようルールを設けることで、y座標の1バイトを削除し32バイトの公開鍵を指定するようになった。これがx-only public key。なので、TaprootなUTXOを使用する場合は、以下のTaprootの構成要素でこのx-only public keyが適用される:

  • TaprootのscriptPubkeyであるOP_1 <公開鍵>で構成されるP2TRアウトプットの<公開鍵>部分。
  • ↑の公開鍵は、内部公開鍵 + tGで構成される。この時、
    • この計算時に内部公開鍵はy座標が偶数となる公開鍵として扱われる
    • tは、内部公開鍵のx座標とTaprootのスクリプトツリーのルートハッシュから作られるタグ付きハッシュで、t = H("TapTweak" || 内部公開鍵(x-only) || merkle root)
  • Taprootのスクリプトツリー内の各スクリプトに登場する公開鍵

x-only public keyの課題

x-only public keyにより、公開鍵が登場する際に1バイト分データを節約できるようになったけど、これによる課題も報告されている↓

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019420.html

具体的には、TAPLEAF_UPDATE_VERIFY opcodeを導入してCovenantsをサポートするという提案で見られる。この提案自体は↓

techmedia-think.hatenablog.com

問題となるのは、TLUVで内部公開鍵を更新するケース。例えば、アリス(A)、ボブ(B)、キャロル(C)がP2TRのコインを共有するプールスキームを考える。

3人の公開鍵A、B、Cのy座標がすべて偶数で、かつA + B + C(内部公開鍵)のy座標も偶数のケースを考える。この場合、P2TRの公開鍵は、

Q = (A + B + C) + H("TapTweak" || (A + B + C) || root1)G

この段階で、Taprootのx-only public keyの要件を満たしていたとしても、プールからCが抜ける場合、更新後の内部公開鍵A + Bのy座標が偶数になるとは限らない。A + Bのy座標が奇数である場合、

Q' = -(A + B) + H("TapTweak" || -(A + B) || root2)G

のような変換が必要になる。ここまでは、まぁ変換すればいいだけと捉えても問題ないが、さらにBがプールから抜ける場合、-(A + B)からBを減算することになり、

Q'' = -A - 2B + H("TapTweak" || -A - 2B) || root3)G

となり、Bがプールから抜けても、keypath支払いをする場合、Bの協力が必要になってしまう。

この回避のために↑の投稿では、内部公開鍵のy座標が奇数になる場合はエラーにして、参加者のすべての鍵の組み合わせでy座標が偶数になるよう事前調整するという手法も提案されてる。参加者(公開鍵)の数をnとした場合、約2nの計算が必要になるとされている。nが30以下くらいであれば計算可能な範囲だけど、まぁそれでも計算コストはかかるのと、事前に鍵の組み合わせの検証を求めるのもハードル高いので、あまり現実的ではないように思える。

ワークアラウンドな回避策

これに対して、Tim Ruffingが提案したのがワークアラウンドな回避策↓

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-July/020663.html

この回避策は、内部公開鍵のy座標が偶数になるまで、ベースポイントGを加算するというもの。

Q = (A + B + C)からCが抜ける場合、Q'のy座標を偶数にするために、

Q' = A + B + G

とする。そして、さらにBが抜ける場合Q'' = A + Gのy座標が奇数であれば、さらに

Q'' = A + G + G = A + 2G

とするというもの。必ず1回Gを加算する訳ではなく、y座標が偶数になるまでGを加算する。↑の投稿では、平均1回の加算で成功する(証明はなし)だろうとしている。

すべての鍵の組み合わせを事前に計算するより、対応が簡単な方法だ。

TLUV自体は、まだ提案中のopcodeだけど、こういうコントラクトを作成するアプローチは十分考えられるので、結果的にx-only public keyはあまり良いアプローチではなかった感じかなー。

IONのDIDのリカバリーと無効化操作をアンカリングしてみる

techmedia-think.hatenablog.com

techmedia-think.hatenablog.com

ときたので、残りのRecoverおよびDeactivate操作をアンカリングしてみる。

Recover操作

Recoverは、Update Keyの更新とRecovery Key自体の更新および、DID State Patcheで定義したDID Documentに対する変更を適用する操作になる。DID Documentの更新という意味ではUpdate操作と似ているけど、Update Keyを紛失したり盗まれたとしても、Recovery Keyがあれば(制御下にあれば)、Recover操作によるDIDの更新で、DIDの管理が引き続き可能。一方、Recovery Keyを紛失してしまうと、既存のUpdate KeyによりDIDの更新だけは引き続き可能だけど、盗まれてしまうとDIDの制御権を奪われることになる。そういう意味でRecover Keyの管理の方が重要度が高い。

実際にRecover操作で、DID Documentを更新してみよう。手順は↓

  1. 新しいRecovery Keyを生成
  2. 新しいUpdate Keyを生成
  3. DID Documentに対する変更を定義したDID State Patcheを作成する。今回は新しい署名鍵を生成し、現在のDIDの署名鍵を新しい鍵に置き換えるreplaceアクションを適用。
  4. 2のUpdate Keyのコミットメントと、3のPatchからRecovery Operation Delta Objectを作成。
  5. 4のデータのハッシュと現在のRecovery Keyのデータ、新しいRecovery KeyのコミットメントからRecovery Operation Signed Data Objectを作成。
  6. 5のデータに対して、現在のRecovery Keyで署名したJWSを生成。
  7. JWSデータを使って、Core Proof Fileを作成し、IPFSにアンカリングする。
  8. 4のDeltaオブジェクトを使って、Chunk Fileを作成し、IPFSにアンカリングする。
  9. 8のChunk FileのURIを含む、Provisional Index Fileを作成し、IPFSにアンカリングする。
  10. 9のProvisional Index FileのURIと、7のCore Proof FileのURI、現在のDIDにコミットされているRecovery Keyのコミットメントに対応するReveal ValueからCore Index Fileを作成し、IPFSにアンカリングする。
  11. Core Index FileのURIからAnchor Stringを生成し、それをBitcoinにアンカリングする。

Update操作との違いは、

  • プルーフはProvisional Proof Fileではなく、Core Proof Fileにセット。
  • Recover操作によるDID Documentの変更の差分(delta)や、更新に使用する鍵のReveal Valueは、Provisional Index FileではなくCore Index Fileに配置。

あたりかな。この辺りのデータの配置先をUpdateとRecover/Deactivateでそれぞれ分離するメリットってどこにあるんだろう?同じファイルや方法で扱えた方がシンプルで便利だと思うんだけど。

以下、実際にRecover操作をCASにアンカリングしたコード↓

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

# DIDのsuffix
did_suffix = "EiBRrmEha_Q30GieEwLB-XM8CZd_b49dQ7znhaBxfAHTsQ"

# 現在設定されているRecovery Key
recovery_key =
  Sidetree::Key.new(
    private_key:
      45_036_086_779_068_677_123_348_498_811_216_758_728_966_810_636_876_259_204_257_311_947_782_243_979_294
  )
# 新しいRecovery Keyを生成
new_recovery_key = Sidetree::Key.generate

# 新しいUpdate Keyを生成
new_update_key = Sidetree::Key.generate

# 新しい署名鍵を生成
new_signing_key = Sidetree::Key.generate(id: "signing-key")

# 置き換えるDID Documentを生成
document = Sidetree::Model::Document.new(public_keys: [new_signing_key])
delta =
  Sidetree::Model::Delta.new(
    [document.to_replace_patch],
    new_update_key.to_commitment
  )
# JWSを生成して現在のRecovery Keyで署名
claim = {
  recoveryKey: recovery_key.to_jwk.normalize,
  recoveryCommitment: new_recovery_key.to_commitment,
  deltaHash: delta.to_hash
}
jws = Sidetree::Util::JWS.sign(claim, recovery_key)

ipfs = Sidetree::CAS::IPFS.new

# Core Proof Fileを作成
core_proof_file = Sidetree::Model::CoreProofFile.new(recover_proofs: [jws])
core_proof_file_uri = ipfs.write(core_proof_file.to_compress)

# Recover Operationを作成
recover_op =
  Sidetree::OP::Recover.new(
    did_suffix,
    delta,
    jws,
    recovery_key.to_reveal_value
  )

# Chunk Fileを作成
chunk_file =
  Sidetree::Model::ChunkFile.create_from_ops(recover_ops: [recover_op])
chunk_file_uri = ipfs.write(chunk_file.to_compress)

# Provisional Index Fileを作成
provisional_index_file =
  Sidetree::Model::ProvisionalIndexFile.new(
    chunks: [Sidetree::Model::Chunk.new(chunk_file_uri)]
  )
provisional_index_file_uri = ipfs.write(provisional_index_file.to_compress)

# Core Index Fileを作成
core_index_file =
  Sidetree::Model::CoreIndexFile.new(
    provisional_index_file_uri: provisional_index_file_uri,
    core_proof_file_uri: core_proof_file_uri,
    recover_ops: [recover_op]
  )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

anchor_str =
  Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri)

Anchor Stringは、

ion:1.QmT2UBzXXsk4gjmUPYvFkQLXgZayfEUq5hpUEGjVRFphgt

で、アンカリングしたBitcoinトランザクションは、8ed80cf58094d12c4fca6dd9fb2ae136278e78afbd65fa108a5db961f762f7ae。ブロックに取り込まれると、IONのResolverでもDID Documentの更新が確認できる↓

DID Document(署名鍵)に加えて、Recovery Commitment、Update Commitmentが更新されている。

Deactivate操作

Deactivate操作は、Recovery Keyを使ってDID Documentを無効化する操作になる。

手順は、他の操作よりシンプルで、

  1. 無効化対象のDIDのSuffixとRecovery KeyでDeactivate Operation Signed Data Objectを作成
  2. 1のデータに対して、現在のRecovery Keyで署名したJWSを生成。
  3. JWSデータを使って、Core Proof Fileを作成し、IPFSにアンカリングする。
  4. 3のCore Proof FileのURI、現在のDIDにコミットされているRecovery Keyのコミットメントに対応するReveal ValueからCore Index Fileを作成し、IPFSにアンカリングする。
  5. Core Index FileのURIからAnchor Stringを生成し、それをBitcoinにアンカリングする。

DID Documentの更新がないので、Provisional Index FileやChunk Fileが不要になる。

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

# 現在設定されているRecovery Key
recovery_key =
  Sidetree::Key.new(
    private_key:
      22_121_807_773_082_901_989_880_604_296_336_998_182_556_647_810_161_279_933_493_862_174_847_957_422_871
  )

# JWSを生成して現在のRecovery Keyで署名
claim = {
  didSuffix: did_suffix,
  recoveryKey: recovery_key.to_jwk.normalize
}
jws = Sidetree::Util::JWS.sign(claim, recovery_key)

ipfs = Sidetree::CAS::IPFS.new

# Core Proof Fileを作成
core_proof_file = Sidetree::Model::CoreProofFile.new(deactivate_proofs: [jws])
core_proof_file_uri = ipfs.write(core_proof_file.to_compress)

# Deactivate Operationを作成
deactivate_op =
  Sidetree::OP::Deactivate.new(
    did_suffix,
    jws,
    recovery_key.to_reveal_value
  )

# Core Index Fileを作成
core_index_file =
  Sidetree::Model::CoreIndexFile.new(
    core_proof_file_uri: core_proof_file_uri,
    deactivate_ops: [deactivate_op]
  )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

anchor_str =
  Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri)

Anchor Stringは、

ion:1.QmWmJ5HWVvga46CcGkem7JTxMZtJrVtsuQDaPmRf3Pi54V

で、アンカリングしたBitcoinトランザクションは、1c18fc397aebd5c1ee0d12a973d8e9fb01d2c2945bf965508480efc422e78a23。ブロックに取り込まれると、IONのResolverでもDID Documentが無効化されたことが分かる(DID Documentの鍵やserviceが削除されている)↓

ということで、Sidetreeに定義された一連の操作(Create / Update / Recover / Deactivate)を一通りやってみた。

ブロックチェーンにアンカリングしたデータを使ってオフチェーンプロトコルを動作させるというコンセプトはやっぱり面白い。ブロックチェーンで担保するのはデータの適用順序だけ、後はオフチェーンのコントラクトやプロトコルで解決する。

IONのDIDを更新してみる

前回の記事でIONのDIDを登録したので↓

techmedia-think.hatenablog.com

今回は、登録したDIDの鍵を更新するUpdate操作をアンカリングしてみる。

Update操作

Update操作は、現在DIDに紐付けられているUpdate Keyを使ってDID Documentの更新(署名鍵やserviceの追加、削除など)を行う操作になる。更新の対象や内容は、DID State Patchesで定義する。

今回は、DIDに署名鍵として登録した公開鍵を新しい公開鍵に更新するreplaceアクションを行うことにする(Patchは複数設定することも可能)。

今回のUpdate操作に必要な手順は↓

  1. 新しい署名用の鍵ペアを生成する。
  2. 1の公開鍵のデータを元に、replaceを行うDID State Patcheを生成する。
  3. DIDに紐付ける新しいUpdate Keyを生成する(このUpdate Keyは、この更新後のDIDのステートを更新する際に使用されるUpdate Keyになる)。なので、更新用の鍵はワンタイムの鍵ということになる。
  4. 3の新しいUpdate Keyのコミットメントを生成する。
  5. 2と4のデータからUpdate Operation Delta Objectを生成する。
  6. 現在のUpdate Keyを使って、JWSデータを生成する。
    • ヘッダーはalg: ES256Kのみ指定。
    • JWSのペイロードは、以下の2つの要素で構成される。
      • 現在のUpdate KeyのデータであるupdateKey
      • 5のUpdate Operation Delta Objectのハッシュ値であるdeltaHash
    • ↑のデータに対して、現在のUpdate Keyで署名した署名値をセットする。
  7. 6のJWSのデータを使って、(Create操作では不要だった)Provisional Proof Fileを生成して、IPFSにアンカリングする。
  8. 5のdeltaファイルからChunk Fileを生成して、IPFSにアンカリングする。
  9. 以下のデータからProvisional Index Fileを生成して、IPFSにアンカリングする。
    • 7のProvisional Proof FileのURI
    • 8のChunk FileのURI
    • 更新対象のDID Suffix(Short-Form形式のDID)
    • 現在のDIDにコミットされているUpdate Keyのコミットメントに対応するReveal Value(JWKのSHA-256 Multihash。コミットメントはdoubule-SHA-256されてる)。
  10. 9のProvisional Index FileのURIを指定して、Core Index Fileを生成し、IPFSにアンカリングする。
  11. 10のCore Index FileのURIからAnchor Stringを生成し、それをBitcoinにアンカリングする。

前回のCreate操作と違うのは、Update操作が可能なことを暗号学的に証明するためのJWSの生成と、それをCASにアンカリングするためのProvisional Proof Fileの作成。

実際に操作を実行

前回登録したDIDを更新しようとしたんだけど、いろいろ試行錯誤して試してたら、

{
code: "canonicalized_object_hash_mismatch",
message: "Canonicalized update request update key object hash does not match expected hash 'EiDBQ56irlBskr7ZawFLcoH131ZvSLbfa2jK1MOny-feIQ'."
}

というエラー以降、更新できなくなってしまったので、新しいDIDを作成して(did:ion:test:EiBRrmEha_Q30GieEwLB-XM8CZd_b49dQ7znhaBxfAHTsQ)↓

こちらを更新することにする。Update処理のサポートをsidetreerbに実装して、↓を実行

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

did = generate_did # ↑で新規作成したSidetree::DIDオブジェクト(作成方法は前回の記事参照)

# DID新規作成時に設定したUpdate Key
update_key =
  Sidetree::Key.new(
    private_key:
      30_922_012_418_511_471_117_584_821_339_133_667_644_396_117_517_409_218_592_473_694_736_410_013_586_765
  )

# 新しい署名鍵を生成
new_signing_key = Sidetree::Key.generate(id: "signing-key")

# 置き換えるDID Documentを生成
document = Sidetree::Model::Document.new(public_keys: [new_signing_key])

# 次のUpdate Keyを生成
next_update_key = Sidetree::Key.generate

# Update delta objectを生成
delta =
  Sidetree::Model::Delta.new(
    [document.to_replace_patch],
    next_update_key.to_commitment
  )

# JWSを生成して署名
claim = { updateKey: update_key.to_jwk.normalize, deltaHash: delta.to_hash }
jws = Sidetree::Util::JWS.sign(claim, update_key)

ipfs = Sidetree::CAS::IPFS.new

# Provisional Proof Fileを作成
provisional_proof = Sidetree::Model::ProvisionalProofFile.new([jws])
provisional_proof_uri = ipfs.write(provisional_proof.to_compress)

# Update operationを作成
update_op =
  Sidetree::OP::Update.new(did.suffix, delta, jws, update_key.to_reveal_value)

# Chunk Fileを作成
chunk_file =
  Sidetree::Model::ChunkFile.create_from_ops(update_ops: [update_op])
chunk_file_uri = ipfs.write(chunk_file.to_compress)

# Provisional Index Fileを作成
provisional_index_file =
  Sidetree::Model::ProvisionalIndexFile.new(
    proof_file_uri: provisional_proof_uri,
    chunks: [Sidetree::Model::Chunk.new(chunk_file_uri)],
    operations: [update_op]
  )
provisional_index_file_uri = ipfs.write(provisional_index_file.to_compress)

# Core Index Fileを作成
core_index_file =
  Sidetree::Model::CoreIndexFile.new(
    provisional_index_file_uri: provisional_index_file_uri
  )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

anchor_str =
  Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri)

最終的なAnchor Stringは、

ion:1.QmVKryfVemjT3Nxnea4aJpJRXFJkLpFjNfti1tJngJwBWs

Bitcoinにアンカリングしたトランザクション40ba3f1d10456be725ac176a333e42b0ef9fdf47cb9f325f8f5c4f69b2e931df

ブロックが承認され、IONで取り込まれると、先程のDIDを解決すると、↓のようにdid:ion:test:EiBRrmEha_Q30GieEwLB-XM8CZd_b49dQ7znhaBxfAHTsQの署名鍵が新しく生成した鍵で更新されていることが分かる。

IONのDIDをIPFSとBitcoinにアンカリングしてみる

前回は、IONのDIDを生成した↓

techmedia-think.hatenablog.com

↑では、DID Documentを生成し、IONのプロトコルに沿って識別子であるDIDを計算したけど、実際にこれだけだとローカルで計算してるだけなので、続いて、このDIDをCAS(Content Addressable Storage)とブロックチェーンにアンカリングしてみる。

CASへのアンカリング

Sidetreeでは、以下のファイルでDID操作のデータを管理している。

https://identity.foundation/sidetree/spec/diagrams/file-topology.svg

  • Core Proof File:DIDのRecoverDeactivate操作について、それらが適切な操作であることを証明する暗号学的な証明を含むファイル。具体的には、現在のRecovery Keyとそれに対して有効な署名など(Recoverの場合のみ、次のRecovery Keyのコミットメントを含む)。
  • Provisional Index File:DIDのUpdate操作を証明するデータ(DIDのsuffixデータ、その最新のUpdate Keyのコミットメントに対応するUpdate Key)に加えて、Provisional Proof FileのURIと各Chunk FileのURIが含まれるファイル。
    • Provisional Proof File:Update操作について、それが適切な操作であることを証明する暗号学的な証明を含むファイル。具体的には、現在のUpdate Keyとそれに対して有効な署名が含まれるファイル。
    • Chunk File:DIDの状態を更新する際の、変更のPatchデータや、次のUpdate Keyのコミットメントなどが含まれる。
  • Core Index File:CreateRecoverDeactivateの3つのDID操作の値と、関連するProvisional Index FileのCASのURIが定義されたファイル。RecoverDeactivate操作の場合は、追加でCore Proof FileのURIが含まれる。

この内、最後のCore Index FileのURIブロックチェーンにアンカリングされる。URIからこのファイルを取得すれば、このファイルで行われたDID操作がリストされおり、また各関連ファイルを辿れるようになっている。

新規作成したDIDのアンカリング

今回はDIDのCreate操作をアンカリングしてみる。この場合、作成するのは、Chunk Fileと作成したChunk FileのURIが含まれるProvisional Index Fileおよび、Provisional Index FileのURIとCreateオペレーションのデータが含まれるCore Index Fileの3つ。※ Update/Recover/DeactivateはしないのでProof系のファイルは作らない。

まず、アンカリング対象のDIDを作成(did:ion:test:EiCFPfLlfe_2D8UxjJtGsaE5zsTisGZiOSU16dgq6vHDOw)↓*1

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

recovery_key = Sidetree::Key.generate
update_key = Sidetree::Key.generate
signing_key = Sidetree::Key.generate(id: 'signing-key')

document = Sidetree::Model::Document.new(public_keys: [signing_key])

did = Sidetree::DID.create(document, update_key, recovery_key, method: Sidetree::Params::METHODS[:ion])

Long-FormのDIDは、

Long-Form DID: did:ion:test:EiCFPfLlfe_2D8UxjJtGsaE5zsTisGZiOSU16dgq6vHDOw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWduaW5nLWtleSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJ0TVFEbnFkLUZIRGJjaGxwYzl6b0JsbVlib29hNk9NRVZyNlZTWERhTHpZIiwieSI6IlVzNEtWYy12OFBRYnJWUTRtdjNvczhoVG1lQUZ3OE5XRFJ6TmY4NHltdXMifSwicHVycG9zZXMiOltdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURCUTU2aXJsQnNrcjdaYXdGTGNvSDEzMVp2U0xiZmEyaksxTU9ueS1mZUlRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCeExEX1BVNEVlQlpVNXpKV2wxMGtvTUZmTmtjQTZ4TExvZ0FOWTdfakxKQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ0NhR3l5YU4yOFMydUc4MS1kTjl3c1ZQVTZoUTVYb1AxZFJtQ3pONU9uNUEifX0

↑のDIDを実際にアンカリングしてみる。手順は、

  1. 新規DIDのCreate Operationデータを生成。
  2. 1のデータを使ってChunk Fileを生成。今回のケースだとCreate OperationのDeltaデータが格納される。つまり、登録するDID Documentが含まれるデータ。
  3. Chunk FileをIPFSに書き込み、URIを取得。
  4. 3のURIを持つProvisional Index Fileを生成。今回のケースだとCreateのみなので、2のURIのみ。Updateが含まれる場合は、Provisional Proof FileのURIやオペレーションデータが含まれる。
  5. Provisional Index FileをIPFSに書き込み、URIを取得。
  6. 1のCreate Operationのデータと5のProvisional Index FileのURIからCore Index Fileを生成。今回のケースだとCreateのみなので、5のURIとオペレーションデータのみ。UpdateやRecoverがあれば別途Core Proof FileのURIが含まれる
  7. Core Index FileのURIをIPFSに書き込み、URIを取得。

↑のDIDについて、生成した2のChunk Fileのデータは↓

{"deltas":[{"patches":[{"action":"replace","document":{"publicKeys":[{"id":"signing-key","publicKeyJwk":{"crv":"secp256k1","kty":"EC","x":"tMQDnqd-FHDbchlpc9zoBlmYbooa6OMEVr6VSXDaLzY","y":"Us4KVc-v8PQbrVQ4mv3os8hTmeAFw8NWDRzNf84ymus"},"purposes":[],"type":"EcdsaSecp256k1VerificationKey2019"}],"services":[]}}],"updateCommitment":"EiDBQ56irlBskr7ZawFLcoH131ZvSLbfa2jK1MOny-feIQ"}]}

Provisional Index Fileのデータは↓

{"chunks":[{"chunkFileUri":"QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q"}]}

Core Index Fileのデータは↓

{"provisionalIndexFileUri":"Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo","operations":{"create":[{"suffixData":{"deltaHash":"EiBxLD_PU4EeBZU5zJWl10koMFfNkcA6xLLogANY7_jLJA","recoveryCommitment":"EiCCaGyyaN28S2uG81-dN9wsVPU6hQ5XoP1dRmCzN5On5A"}}]}}

いずれもIPFSに格納されるデータは↑をGZIP圧縮したデータになる。

sidetreerbに実装してみた↓(事前にIPFS daemonをローカルで実行しておく)。

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

# DIDの生成
recovery_key = Sidetree::Key.generate
update_key = Sidetree::Key.generate
signing_key = Sidetree::Key.generate(id: "signing-key")

document = Sidetree::Model::Document.new(public_keys: [signing_key])

did = Sidetree::DID.create(
      document,
      update_key,
      recovery_key,
      method: Sidetree::Params::METHODS[:ion]
    )

# DIDのCreate Operationを生成
create_op = did.create_op

# Chunk Fileを生成
chunk_file =  Sidetree::Model::ChunkFile.create_from_ops(create_ops: [create_op])

# IPFSクライアントを初期化(パラメータ省略するとデフォルトでhttp://localhost:5001/api/v0にアクセス)
ipfs = Sidetree::CAS::IPFS.new

# Chunk FileをIPFSに書き込み
chunk_file_uri = ipfs.write(chunk_file.to_compress)

# Provisional Index Fileを作成しIPFSに書き込み
provisional_index_file = Sidetree::Model::ProvisionalIndexFile.new(chunks: [Sidetree::Model::Chunk.new(chunk_file_uri)])
provisional_index_file_uri = ipfs.write(provisional_index_file.to_compress)

# Core Index Fileを作成しIPFSに書き込み
core_index_file = Sidetree::Model::CoreIndexFile.new(
          create_ops: [create_op],
          provisional_index_file_uri: provisional_index_file_uri
        )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

最終的なCore Index FileのURIは、

QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW

Bitcoinへのアンカリング

CASへのアンカリングが出来たら、続いてブロックチェーンにアンカリングする。ここで、ブロックチェーンにアンカリングするデータは、Anchor Stringとして定義されているデータで、↑で作成したCore Index FileのURIと、操作の数で構成される。CASにアンカリングされた関連操作はCore Index Fileで参照可能になっているので、そのURIブロックチェーンにアンカリングしている。

Anchor Stringは、単純に操作数とCore Index FileのURI.で連結したデータ(例:1000.QmWd5PH6vyRH5kMdzZRPBnf952dbR4av3Bd7B2wBqMaAcf)。

なので、今回は、Anchor Stringにprefix(ion:)を付けた以下のデータをOP_RETURNを使ってBitcoinトランザクションに埋め込めばいい。

ion:1.QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW

実際に発行したトランザクション45e4459c7f3a36124b8b5df5c5b9314c4187833056f89a0c88ad57997fe1b051。OP_RETURNに↑が記録されているのが分かる。

ちなみに、↑のアンカリングTxの手数料が最低額を下回ってるとBitcoin的にはブロックに格納されるけど、IONとしては有効なCoreファイルとしてエラー処理される。

トランザクションがブロックに格納されると、ionノードを起動していれば新しいブロック内のアンカリングトランザクションを検知し、DIDの処理が行われる↓

Fetched 1 Sidetree transactions from blockchain service in 42 ms.
CommandSucceededEvent {
  connectionId: 'localhost:27017',
  requestId: 13,
  commandName: 'find',
  duration: 28,
  reply: {
    cursor: { firstBatch: [Array], id: 0, ns: 'ion-testnet-core.transactions' },
    ok: 1
  }
}
Downloading core index file 'QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW', max file size limit 1000000 bytes...
Downloading file 'QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW', max size limit 1000000...
Successfully kicked off downloading/processing of all new Sidetree transactions.
Processing previously unresolvable transactions if any...
CommandSucceededEvent {
  connectionId: 'localhost:27017',
  requestId: 14,
  commandName: 'find',
  duration: 1,
  reply: {
    cursor: {
      firstBatch: [],
      id: 0,
      ns: 'ion-testnet-core.unresolvable-transactions'
    },
    ok: 1
  }
}
Fetched 0 unresolvable transactions to retry in 1 ms.
Event emitted: sidetree_observer_loop_success
Waiting for 60 seconds before fetching and processing transactions again.
Read and pinned 240 bytes for CID: QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW.
Event emitted: sidetree_download_manager_download: {"code":"success"}
File 'QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW' of size 240 downloaded.
Downloading provisional index file 'Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo', max file size limit 1000000...
Downloading file 'Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo', max size limit 1000000...
Read and pinned 93 bytes for CID: Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo.
Event emitted: sidetree_download_manager_download: {"code":"success"}
File 'Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo' of size 93 downloaded.
Downloading chunk file 'QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q', max size limit 10000000...
Downloading file 'QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q', max size limit 10000000...
Read and pinned 308 bytes for CID: QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q.
Event emitted: sidetree_download_manager_download: {"code":"success"}
File 'QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q' of size 308 downloaded.
Parsed chunk file in 1 ms.
CommandSucceededEvent {
  connectionId: 'localhost:27017',
  requestId: 23,
  commandName: 'update',
  duration: 1,
  reply: { n: 1, upserted: [ [Object] ], nModified: 0, ok: 1 }
}
Processed 1 operations. Retry needed: false
Transaction 1.QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW is confirmed at 2315260

そして、ローカルで実行中のIONにDIDを問い合わせると↓、DIDの解決に無事成功!

http://localhost:3000/identifiers/did:ion:test:EiCFPfLlfe_2D8UxjJtGsaE5zsTisGZiOSU16dgq6vHDOw

今回の最小限のCreate操作で、CASやブロックチェーンにどうデータをアンカリングしているのかが大体分かってきた。

*1:前回作成したDIDは、DID Document内の鍵のtypeの設定が漏れてたので再生成。