はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、1つの秘密情報からサービスごとに異なる認証鍵を安全に導出し、チャレンジを通じて本人確認を行う認証方式を標準化する提案しているSLIP0013についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他に様々なEIPについてまとめています。

概要

SLIP0013では、Webサイトやリモートシェル(SSHなど)への認証を行うための方法として、「決定性階層(Deterministic Hierarchy)」を利用する仕組みについて説明されています。 決定性階層とは、1つの秘密情報(例えばマスターキー)から階層的に複数の鍵を導出できる構造を指します。 これにより、異なるサービスごとに一意の認証情報を持たせつつも、それらすべてを一つの元データから再現できる特性があります。

つまり、ユーザーは複数のサービスやデバイスに対して個別の認証情報を使用しながらも、管理のためには1つの元情報を安全に保管・バックアップしておくだけでよくなります。

動機

この仕組みを使うことで、ユーザーの認証情報(ログイン情報など)の管理やバックアップが非常に簡単になります。 もともと暗号通貨のウォレットで使われている「決定性ウォレット」と同じ考え方で、ひとつのマスターキーさえ安全に保管すれば、そこからすべての秘密鍵を再生成できるというメリットがあります。

この考え方を認証の仕組みに応用することで、パスワードやSSHキーのような重要情報をサービスごとに安全に分離しつつ、一括してバックアップ・復元できる環境を実現できます。 個別のサービスごとに鍵を生成しながらも、それらすべてを中央で管理できるという利便性とセキュリティを両立した設計です。

サービス識別子(Service Identity)

この仕組みでは、特定のサービスに対応した鍵を生成するために、「サービス識別子(Service Identity)」という概念を導入します。 これは以下の2つの要素で構成されます。

1つ目は URI(Uniform Resource Identifier) です。 これは RFC 3986 で定義されている形式で、以下のような書式で表されます。

text
proto://[user@]host[:port][/path]

例えば、以下のようなURIがサービス識別子として利用できます。

  • https://example.com
  • ftp://public@example.com/pub
  • ssh://root@example.com:2222

2つ目は インデックス(index)で、32ビットの符号なし整数です。 このインデックスは、同じURIに対して複数の鍵を生成したい場合に利用されます。 例えば、index = 0 で最初の鍵、index = 1 で2つ目の鍵、というようにバリエーションを持たせることができます。

HD構造(HD Structure)

この仕組みでは、サービス識別子を使って階層的決定性(HD: Hierarchical Deterministic)な鍵を導出します。 以下はその手順です。

  1. まず、インデックス(index)をリトルエンディアン形式でエンコードしたバイト列と、文字列としてのURIを結合します。
  2. その結合した文字列全体に対して、SHA-256ハッシュ関数を適用します。これによって一意のハッシュ値が得られます。
  3. 得られたSHA-256ハッシュ値のうち、最初の128ビット(16バイト)だけを使用します。この部分をhash128とします。
  4. hash128を4つの32ビット整数(A, B, C, D)に分割します。このときもリトルエンディアン形式を使用します。
  5. 各整数の最上位ビット(31番目のビット)を1に設定します。これは「ハード化(hardened)」と呼ばれる処理で、外部からの鍵導出を防ぐための安全性の確保手段です。
  6. 最後に、m/13'/A'/B'/C'/D' という形式で**SLIP0010*&に準拠したHDパスを組み立てます。このパスに従って秘密鍵を導出することで、サービスごとに一意かつ再現可能な鍵を得ることができます。

リトルエンディアン(Little Endian)とは数値をバイト列に分解するときに「下位のバイト(小さい桁)から先に並べる方式」のことです。 コンピュータで数値を扱う時に、メモリやデータ通信でどの順序で並べるかを決める「エンディアン(Endian)」という考え方の一種です。

例えば、次のような 16進数の32ビット整数(4バイト)を考えます。

text
0x12345678

これをバイト単位で分けると以下のようになります。

  • 0x12(最上位バイト)
  • 0x34
  • 0x56
  • 0x78(最下位バイト)

ビッグエンディアン(Big Endian)

この順番のまま(上位→下位)で並べる方式。

text
[0x12, 0x34, 0x56, 0x78]

リトルエンディアン(Little Endian)

逆に、下位バイトを先に並べる方式。

text
[0x78, 0x56, 0x34, 0x12]

実例(Worked Example)

具体的な例として、以下のサービス識別子を使います。

  • URI: https://satoshi@bitcoin.org/login
  • index: 0

まず、インデックスをバイト表現(リトルエンディアン)し、それを文字列URIに連結します。 この結果にSHA-256を適用すると、以下のようなハッシュが得られます。

text
d0e2389d4c8394a9f3e32de01104bf6e8db2d9e2bb0905d60fffa5a18fd696db

このハッシュの先頭128ビット(16バイト)を取り出すと以下の値になります。

text
d0e2389d4c8394a9f3e32de01104bf6e

この値をリトルエンディアンで4つの32ビット整数に分割すると以下のようになります。

  • A = 2637750992
  • B = 2845082444
  • C = 3761103859
  • D = 1858012177

このうちDのみ最上位ビットが0だったため、ハード化するためにこのビットを1にします。 結果的に以下のようになります。

  • A' = 2637750992
  • B' = 2845082444
  • C' = 3761103859
  • D' = 4005495825(元が1858012177)

最終的なHDパスは以下のようになります。

text
m/2147483661/2637750992/2845082444/3761103859/4005495825

このパスに基づいて鍵を導出することで、「このサービス用の鍵」を確実に再現することができます。

補足

この方法は、SLIP-0010という規格に基づいており、秘密鍵の導出と管理を統一的に行うための標準的な仕組みです。

Pythonでの具体的な実装例は、以下のTrezorのテストコードで確認できます。 Python Example - test_msg_signidentity.py

チャレンジ・レスポンス認証の仕組み

決定性階層(HD構造)から導出される鍵を用いた「チャレンジ・レスポンス認証」は、サービスにログインする際やユーザーを識別する時に安全かつ再現可能な方法で認証を行うための仕組みです。

チャレンジの構成

サービスは、認証を要求するユーザーに対して「チャレンジ(challenge)」を発行します。 このチャレンジは3つの要素で構成されます。

サービス識別子(Service Identity)

前の章で説明したとおり、特定のサービスを一意に識別するための情報で、URIとインデックスからなります。 例えば、https://example.com 0 のような文字列です。 この識別子は、そのサービスに対して導出すべき秘密鍵を決定する際に使用されます。

隠しチャレンジ(Hidden Challenge)

これは最大64バイトのランダムなバイト列で、ユーザーには表示されません。 サービス側が内部的に認証処理を行うために使用します。

ビジュアルチャレンジ(Visual Challenge)

こちらはユーザーにも表示される文字列で、最大64文字まで設定できます。 ユーザーにとっての識別用に、YYYY-MM-DD HH:MM:SS 形式のタイムスタンプなどを表示することが推奨されています。

署名の生成プロセス

ユーザー(または署名者)は、上記3つの情報を受け取ったうえで、以下の処理を行います。

まず、サービス識別子に基づいて、HD構造から秘密鍵を導出します。 次に、以下のように署名対象のデータを生成します。

  1. 隠しチャレンジに対してSHA-256ハッシュを計算します。
  2. ビジュアルチャレンジに対しても同様にSHA-256ハッシュを計算します。
  3. この2つのハッシュを連結します。
  4. 結果のバイト列を、標準的なBitcoinメッセージ署名方式を使って秘密鍵で署名します。

その後、以下の3点をまとめてサービス側に返します。

  • 署名済みデータ
  • 公開鍵
  • 公開鍵に対応するアドレス(ノードアドレス)

サービス側の対応

サービス運営者は、受け取った署名と公開鍵に応じて、以下のいずれかの対応を行います。

  • 署名が無効または存在しない場合 ユーザーにエラーを表示して、認証を拒否します。
  • 署名が有効で、公開鍵やアドレスが初めて見つかった場合 新しいユーザーとしてアカウントを作成します。
  • 署名が有効で、すでに登録されているアドレスの場合 対応する既存のアカウントにログインさせます。

最後に

今回は「1つの秘密情報からサービスごとに異なる認証鍵を安全に導出し、チャレンジを通じて本人確認を行う認証方式を標準化する提案しているSLIP0013」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!