Symbol と Ledger を使ってウェブアプリでユーザーを認証する

[nem Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nem) の14日目の記事です。

## Symbol と Ledger を使ってウェブアプリでユーザーを認証する

ブロックチェーン Symbol とハードウェアウォレット Ledger Nano S を使用して、ウェブアプリへのログインを行います。

:point_down::point_down::point_down: 動作イメージの動画です :vhs: :vhs: :vhs:

https://twitter.com/takasaki_bc/status/1407260057079664640


実際に動いてるものはこちらから👉 [https://adventcalendar2021-461a9.web.app/](https://adventcalendar2021-461a9.web.app/)

:::note alert
テストネットのアドレスの署名を検証してすぐに破棄しているため安全だとは思いますが、資産を保管している Ledger を使用する際にはくれぐれもご注意ください。資産性のあるものが何も入ってない Ledger の使用をお勧めします
:::

## 自己紹介

高崎と申します。ダルマと呼ばれたり ⭕️ と呼ばれたりします → [Twitter](https://twitter.com/takasaki_bc)

過去には NEM で変なものを色々作りました。

- [NEM でピクセルアートを NFT 化した「閉鎖国家ピユピル」] (https://crypto.watch.impress.co.jp/docs/column/akiohoshi/1171564.html)
- [NEM に自分の所在地を記録する装置「ブロックチェーン上に天国を作る（プロトタイプ）」](https://www.youtube.com/watch?v=dau3o5unPNA)
- [書くと消せないスレッド式掲示板「SNEMS」](https://snems.com)

Symbol ではあまり変なことをしていません。

## 何がしたいのか
Ethereum や EVM 互換チェーンではブラウザのプラグイン型ウォレットである MetaMask を中心としたエコシステムができあがっています。DeFi や NFT の盛り上がりも　MetaMask 無しにはありえなかったのではないでしょうか。

ところが Symbol にはブラウザに組み込める標準的なウォレットは無く、ウェブアプリとの連携はデスクトップウォレットやモバイルウォレットを使用することが多いようです（2021年12月現在）。数年前は「使いやすい」が売りだった NEM/Symbol ですが、現在ではウェブアプリからは比較的使いにくい部類のチェーンになっているように見えます。

使いやすいウォレットの開発が進んでいるというような話も聞きますが、それを待たずに今あるものでウェブとの連携はできないでしょうか。

## Ledger をユーザー認証に使う

Ledger はハードウェアウォレットです。秘密鍵を外部に晒すことなくトランザクションに署名ができるため、安全に送金を行うことができます。

https://www.youtube.com/watch?v=rzN3IDztv5U

現在購入できるのは

- USB 接続の Ledger Nano S
- Bluetooth 接続の Ledger Nano X

の２種類です。今回は USB 接続の Ledger Nano S を使用しています。

ハードウェアウォレットは本来「資産の安全な保管」という目的で使用されますが、今回はユーザー認証のためのデバイスとして捉えてみます。

つまり「Ledger を使って Symbol アドレスの所有を証明する」ことでユーザー認証を行うという寸法です。MetaMask を使ったユーザー認証も同様の仕組みを使っていますので、ウェブアプリから Ledger に接続しトランザクションに署名ができれば代わりになりそうですね。[^1]

[^1]: ちなみに Ethereum の世界だと　MetaMask でのログインと並んで普通に Ledger でのログインがあったりするので特に目新しいことをしているわけではありません 例→[Compound](https://app.compound.finance/) 

## ブラウザと通信する

https://github.com/fboucquez/symbol-ledger-typescript

ブラウザとの通信にはこちらのライブラリを使います。TypeScript から Ledger を扱えるようにしてくれています。

README.md にはターミナルからの接続テスト方法が書いてあります。ウェブブラウザと接続する場合は WebUSB API を使用し、 Human interface device (HID) として Ledger を制御します。

### Ledger に Symbol アプリを入れる

Ledger を使うためには　Ledger 本体に各チェーン用のアプリを入れる必要があります。

まずは Ledger の管理用アプリ「Ledger Live」をインストールし、Manager タブから Symbol アプリをインストールしましょう。

https://www.ledger.com/ledger-live/download

### コードを書く

Ledger に Symbol アプリをインストールしたらウェブアプリ側のコードを書きましょう。
ライブラリのテストコードを参考に書いていきます。（UI に関するコードは割愛します）

1. パッケージのインストール
1. アドレスの取得
1. 署名と検証

#### パッケージのインストール

Symbol と Ledger 関連のパッケージをインストール
（プロジェクトの作成や TypeScript の環境構築は割愛）

```bash
npm install --save symbol-ledger-typescript
npm install --save @ledgerhq/hw-transport-webhid
npm install --save symbol-sdk
npm install --save-dev @types/w3c-web-hid
```

これで準備完了です。


#### アドレスの取得

では接続してみましょう。

まずは Ledger を PC に接続して　PIN を入力してから Symbol アプリを開いてください

:::note warn
Ledger はすぐにスリープしてしまいます。開発中に動作がおかしいなと思ったらまずは Ledger がスリープしていないか確認しましょう。
:::

サンプルコードではターミナルで Node から Ledger に接続するために TransportNodeHid クラスを使用していますが、ブラウザから接続する場合は代わりに TransportWebHID クラスを使用します。



```typescript
import TransportWebHID from '@ledgerhq/hw-transport-webhid'

import {
    Address,
    Convert,
    Deadline,
    KeyPair,
    NetworkType,
    PlainMessage,
    TransferTransaction,
    UInt64,
} from 'symbol-sdk'

import { LedgerDerivationPath, LedgerNetworkType, SymbolLedger } from 'symbol-ledger-typescript'

/* （UIに関するコードは割愛） */

async function getAccounts(accounts = 5): Promise<Array<String>> {
    const res = Array<String>()
    await TransportWebHID.create(5000, 5000).then(async transport => {
        const ledger = new SymbolLedger(transport, 'XYM')
        try {
            const ledgerNetworkType = LedgerNetworkType.TEST_NET
            const networkType = ledgerNetworkType as number as NetworkType
            for (const accountIndex of Array.from(Array(accounts).keys())) {
                const path = LedgerDerivationPath.getPath(ledgerNetworkType, accountIndex)
                const publicKey = await ledger.getAccount(path, ledgerNetworkType, false, false, false)

                res.push(Address.createFromPublicKey(publicKey, networkType).plain())
            }
        } finally {
            await ledger?.close()
        }
    })
    return res
}
```

手順としては

1. TransportWebHID.create でデバイスに接続
1. SymbolLedger(transport, 'XYM') で Symbol アプリ接続用のインスタンスを作成
1. LedgerDerivationPath.getPath で derivation path を取得
1. SymbolLedger#getAccount　に derivation path を渡して public key を取得
1. Address.createFromPublicKey　に public key を渡してアドレスを取得

という流れになっています。（上記のコードでは複数のアドレスを取得しています）

これを実行するとブラウザにダイアログで PC に接続されている HID 機器の一覧が表示されるので、接続したい Ledger を選択します。

これでアドレスが取得できました。


#### 署名と検証

次は Ledger でトランザクションに署名を行い、そのトランザクションを検証することでアドレスの所有者かどうかをチェックします。（UIに関する処理は割愛します）

```typescript
const deadline = Deadline.createFromDTO('1000')
const generationHash = '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836'

async function createTransferTransaction(ledgerNetworkType: LedgerNetworkType, networkType: NetworkType) {
    const recipientPublicKey = '41A27D66F16AA1CD07CAB6905392DCCB34FBDFB8CA3047E0CB06F32806F0E45F'
    const maxFee = UInt64.fromUint(0)
    return TransferTransaction.create(
        deadline,
        Address.createFromPublicKey(recipientPublicKey, networkType),
        [],
        PlainMessage.create('Log in'),
        networkType,
        maxFee,
    )
}

const verifySignature = (publicKey: string, data: string, signature: string): boolean => {
    return KeyPair.verify(Convert.hexToUint8(publicKey), Convert.hexToUint8(data), Convert.hexToUint8(signature))
}

const ledgerNetworkType = LedgerNetworkType.TEST_NET
const networkType = ledgerNetworkType as number as NetworkType
const generationHashBytes = Array.from(Convert.hexToUint8(generationHash))
const transferTransaction = await createTransferTransaction(ledgerNetworkType, networkType)
const payloadBytes = Array.from(Convert.hexToUint8(transferTransaction.serialize()))
const signingBytes = transferTransaction.getSigningBytes(payloadBytes, generationHashBytes)
    
TransportWebHID.create(5000, 5000).then(async transport => {
    const ledger = new SymbolLedger(transport, 'XYM')
    try {
        const path = LedgerDerivationPath.getPath(ledgerNetworkType, 0)/* ログインに使うアドレスの derivation path を指定します */
        const publicKey = await ledger.getAccount(path, ledgerNetworkType, false, false, false)
        const { signature } = await ledger.signTransaction(path, transferTransaction, generationHash, publicKey, false)

        if (verifySignature(publicKey, Convert.uint8ToHex(signingBytes), signature) === true) {
            /* 署名の検証に成功した時のログイン処理 */
        } else {
           /* 検証失敗時の処理 */
        }
    } finally {
        await ledger?.close()
    }
})
```

SymbolLedger#signTransaction が実行されると　Ledger 本体の画面に署名を承認するかの確認が表示されます。確認の上で承認するとログインに成功します。

実際に動いてるものはこちらから👉 [https://adventcalendar2021-461a9.web.app/](https://adventcalendar2021-461a9.web.app/)

:::note alert
テストネットのアドレスの署名を検証してすぐに破棄しているため安全だとは思いますが、資産を保管している Ledger を使用する際にはくれぐれもご注意ください。資産性のあるものが何も入ってない Ledger の使用をお勧めします
:::

## まとめ

ウェブブラウザから Ledger に接続してアドレスを取得し、署名を検証することでユーザー認証を行いました。

これで冒頭の動画のように Ledger を使用してアドレスの所有を確認することができました。ゲームのログインに使うもよし、独自ウォレットの署名に使うもよし。ウェブに限らず会員制クラブへの入館の鍵として使うもよし。

ちなみに下記のツイートの通り、今回使用したライブラリは開発中のものだそうですので、ご使用時にはご留意下さい。半年くらい開発が止まってるっぽいので、実際にサービスで使いたいそこの貴方はグイグイ貢献していきましょう。

https://twitter.com/FBoucquez/status/1407317942648324104