TypeScript で Hyperledger Indy のネットワークに署名検証用の鍵を登録してみた

# この記事の目的

TypeScript からライブラリを呼び出し、Hyperledger Indy のネットワークに署名検証用の鍵を登録する。

~~本当は Javascript でいいと思うけど、TypeScript の記事を募集しているようなので TypeScript にした。~~

# Hyperledger Indy とは

分散アイデンティティのためのブロックチェーン基盤。
ID (DID) に対応する公開鍵を登録し、対応するソフトウェアから利用可能にすることができる。

クレデンシャル (証明書) を発行し、発行したクレデンシャルに関する情報を記録して検証できるようにする機能もあるらしいが、まだ筆者はよくわかっていない。

Hyperledger Indy のネットワークは、複数公開されている。
今回は、それらの中で、[Indicio TestNet](https://indyscan.indiciotech.io/home/IND_TESTNET3) を用いる。

* [Indy](https://www.lfdecentralizedtrust.org/projects/hyperledger-indy)
* [Hyperledger Indy / Aries / Ursaについてまとめた #Blockchain - Qiita](https://qiita.com/pirodate/items/7615fa71f59ae148c5f8)
* [Hyperledger Indy/Aries まとめ #DID - Qiita](https://qiita.com/ito_wokashi/items/64a52434294b0063e73a)

# TypeScript を使う準備をする

TypeScript、および npm で公開されている JavaScript のライブラリを利用する準備をする。

[Node.js](https://nodejs.org/ja) がインストールされていない場合は、インストールする。

作業用の (新しい) ディレクトリを用意し、そこに以下の内容の `package.json` というファイルを用意する。
インストールしたライブラリの情報が、ここに記録される。

```json:package.json
{}
```

同じディレクトリに、以下の内容の `tsconfig.json` というファイルを用意する。
これにより、TypeScript がこのディレクトリ以下のファイルを自動で一括変換できるようにするとともに、ESモジュールを扱えるように設定する。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "moduleResolution": "node"
  }
}
```

TypeScript (トランスパイラ) をインストールする。
また、本体をインストールするだけではインストールされない型情報もインストールする。
これらは開発時に用い、実行時には不要であるため、`-D` をつけてインストールする。

```shell
npm i -D typescript @types/node @types/ref-napi
```

さらに、この記事で用いるライブラリをインストールする。

```shell
npm i bs58 @hyperledger/indy-vdr-nodejs
```

:::note info
`@hyperledger/indy-vdr-nodejs` は、環境によってはインストールできないことがある。
たとえば、Docker の [`node` イメージ](https://hub.docker.com/_/node)では、インストールできるようである。

```shell
docker run --rm -it --entrypoint /bin/bash node:22
```

起動コマンドは、`sudo` を追加したり、ボリュームを追加するなど、環境に応じて調整するといいだろう。
:::

ソースコードの拡張子は、`.mts` とする。
これは、モジュールとして処理する TypeScript のソースコードであることを表す。

ディレクトリ内で

```shell
npx tsc
```

を実行すると、ディレクトリ内の `.mts` ファイルを変換し、JavaScript として実行できる `.mjs` ファイルを作成できる。

# Endorser (書き込み承認者) の鍵とIDを用意する

Hyperledger Indy のネットワークに書き込むには、Endorser と呼ばれる承認者による Ed25519 署名が必要である。
Indicio TestNet では、Endorser の鍵とIDを登録する機能があるので、これを用いて登録を行う。

まず、Endorser による署名に用いる Ed25519 鍵を生成する。
これは、Node.js の [`crypto.generateKeyPairSync`](https://nodejs.org/api/crypto.html#cryptogeneratekeypairsynctype-options) を用いて生成できる。
この関数は、公開鍵 `publicKey` と秘密鍵 `privateKey` が格納されたオブジェクトを返す。

次に、生成した鍵に対応するIDを生成する。
これは、「公開鍵を表す32バイトのデータのSHA-256ハッシュ値の前半16バイトをBase58エンコードした文字列」である。([`did:indy` 用の形式](https://hyperledger.github.io/indy-did-method/#nym-transaction-version))
32バイトのデータは、`generateKeyPairSync` で生成した `publicKey` オブジェクトで `.export({format:"jwk"})` を呼び出すと得られるデータの `x` を Base64url デコードすることで得られる。
SHA-256 ハッシュ値は、Node.js の [`crypto.createHash`](https://nodejs.org/api/crypto.html#cryptocreatehashalgorithm-options) を用いて計算できる。
Base58 エンコードは、[`bs58` ライブラリ](https://www.npmjs.com/package/bs58)を用いてできる。

また、登録時に用いるため、公開鍵のデータを Base58 エンコードしておく。

これらを行う以下のコードを用意した。

```typescript:generateKeyAndId.mts
import { generateKeyPairSync, createHash } from "node:crypto";
import bs58 from "bs58";

export function generateKeyAndId() {
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
  const { x } = publicKey.export({ format: "jwk" });
  const publicKeyData = Buffer.from(x, "base64url");
  const verkey: string = bs58.encode(publicKeyData);
  const hasher = createHash("sha256");
  hasher.update(publicKeyData);
  const publicKeyHash = hasher.digest();
  const id: string = bs58.encode(publicKeyHash.slice(0, 16));
  return {
    id,
    verkey,
    publicKey,
    privateKey,
  };
}
```

`generateKeyAndId` 関数は、以下のメンバを持つオブジェクトを返す。

* `id`：ネットワークで用いるID
* `verkey`：ネットワークで用いる署名検証鍵
* `publicKey`：生成した Ed25519 の公開鍵
* `privateKey`：生成した Ed25519 の秘密鍵

Node.js の対話型実行環境で実行すると、たとえば以下の実行結果が得られる。
(秘密鍵のデータは伏せてある)

```javascript
> { generateKeyAndId } = await import("./generateKeyAndId.mjs")
[Module: null prototype] {
  generateKeyAndId: [Function: generateKeyAndId]
}
> info = generateKeyAndId()
{
  id: '6hjXXCBHBpA2xn1EL55zKR',
  verkey: '4gjjAiUVecKuc689nE7awvmrBKENzxnoP59sQPreBv93',
  publicKey: PublicKeyObject [KeyObject] { [Symbol(kKeyType)]: 'public' },
  privateKey: PrivateKeyObject [KeyObject] { [Symbol(kKeyType)]: 'private' }
}
> info.publicKey.export({format:"jwk"})
{
  crv: 'Ed25519',
  x: 'NsEGIJ2BJoNxyjWk365iMreA-lzUKN8jAO5MVGUv48Y',
  kty: 'OKP'
}
> info.privateKey.export({format:"jwk"})
{
  crv: 'Ed25519',
  d: '*******************************************',
  x: 'NsEGIJ2BJoNxyjWk365iMreA-lzUKN8jAO5MVGUv48Y',
  kty: 'OKP'
}
```

鍵とIDを生成したら、これをネットワークに Endorser として登録する。
Indicio TestNet の場合は、[Get An Indicio Network Endorser!](https://selfserve.indiciotech.io/) から登録できる。
[このネットワークに書き込む際の同意事項](https://github.com/Indicio-tech/indicio-network/blob/main/TAA/TAA.md)に同意した上で、フォームに以下の内容を入力し、「Submit」を押す。

|項目|入力する内容|
|---|---|
|Network|Indicio TestNet|
|DID|上のコードで生成した `id`|
|Verkey|上のコードで生成した `verkey`|

……と、以下のエラーが出て登録に失敗してしまった。

> {"statusCode":400,"headers":{"Access-Control-Allow-Origin":"*"},"body":"{\\"6hjXXCBHBpA2xn1EL55zKR\\": [\\"Full verkey 4gjjAiUVecKuc689nE7awvmrBKENzxnoP59sQPreBv93 must be a 32 byte base58 encoded string.\\"], \\"statusCode\\": 400}"}

さらに試した結果、今回使用したバージョン 2 形式のIDではなく、バージョン 1 形式のID (公開鍵の前半16バイトを Base58 エンコードした文字列) を用いることで、登録することができた。

具体的には、DID としてかわりに `7m9sSPa5FbfvGgYMhoe4Ro` を入力すると、

> {"statusCode":200,"headers":{"Access-Control-Allow-Origin":"*"},"body":"{\\"statusCode\\": 200, \\"7m9sSPa5FbfvGgYMhoe4Ro\\": {\\"status\\": \\"Success\\", \\"statusCode\\": 200, \\"reason\\": \\"Successfully wrote NYM identified by 7m9sSPa5FbfvGgYMhoe4Ro to the ledger with role ENDORSER\\"}}"}

と出て登録に成功した。

登録失敗時のエラーメッセージは、verkey として入力する文字列の**必要条件**は示しているが、DID として入力する文字列との関係を含む**十分条件**は示しておらず、今回はこのメッセージに載っていない条件で弾かれたようなので、**不親切**である。

verkey からバージョン 1 形式のIDへの変換は、以下の recipe を用いた。
[From Base58, Take bytes, To Base58 - CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Base58%28%27123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz%27,true%29Take_bytes%280,16,false%29To_Base58%28%27123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz%27%29&input=NGdqakFpVVZlY0t1YzY4OW5FN2F3dm1yQktFTnp4bm9QNTlzUVByZUJ2OTM&oeol=VT)

# 登録したデータを確認する

[Universal Resolver](https://dev.uniresolver.io/) を用いて、今回登録した Endorser のデータを確認してみる。
今回登録した `7m9sSPa5FbfvGgYMhoe4Ro` はネットワーク上でのIDであり、これに DID の種類 `did:indy:` とネットワークの識別子 `indicio:test:` をつなげた

```text
did:indy:indicio:test:7m9sSPa5FbfvGgYMhoe4Ro
```

が今回登録した Endorser の [DID](https://www.w3.org/TR/did-1.0/) となる。
これを Universal Resolver の did-url 欄に入力し、「Resolve」を押すと、以下の DID DOCUMENT が得られる。

```json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/ed25519-2018/v1"
  ],
  "id": "did:indy:indicio:test:7m9sSPa5FbfvGgYMhoe4Ro",
  "verificationMethod": [
    {
      "type": "Ed25519VerificationKey2018",
      "id": "did:indy:indicio:test:7m9sSPa5FbfvGgYMhoe4Ro#verkey",
      "controller": "did:indy:indicio:test:7m9sSPa5FbfvGgYMhoe4Ro",
      "publicKeyBase58": "4gjjAiUVecKuc689nE7awvmrBKENzxnoP59sQPreBv93"
    }
  ],
  "authentication": [
    "did:indy:indicio:test:7m9sSPa5FbfvGgYMhoe4Ro#verkey"
  ]
}
```

:::note info
Indicio TestNet のデータは定期的にリセットが行われることになっており、時間が経つとデータが得られなくなる可能性がある。
:::

[`verificationMethod`](https://www.w3.org/TR/did-1.0/#dfn-verificationmethod) の部分に今回登録した公開鍵のデータがあり、[`authentication`](https://www.w3.org/TR/did-1.0/#dfn-authentication) の部分でこの鍵をログインなどの認証に用いることができることが宣言されている。
これは、`did:indy` の扱い方の規定内の [Base DIDDoc Template](https://hyperledger.github.io/indy-did-method/#base-diddoc-template) に基づいているようである。

Verifiable Credential への署名に用いることができることを宣言する [`assertionMethod`](https://www.w3.org/TR/did-1.0/#dfn-assertionmethod) は、ここには含まれていない。
そのため、単純にネットワークに (DID用の) IDと公開鍵を登録するだけでは、そのID (DID) に基づいて Verifiable Credential の署名の検証をしてもらうことができない。
そこで、次章では、`assertionMethod` を含むデータの登録を行う。

# ネットワークに署名検証用の鍵を含むデータを登録する

[`@hyperledger/indy-vdr-nodejs` ライブラリ](https://www.npmjs.com/package/@hyperledger/indy-vdr-nodejs) を用いて、Hyperledger Indy のネットワークにアクセスし、署名検証用の鍵を含むデータを登録する。

ネットワークの詳しい操作方法は、以下のページに情報がある。

* [Requests — Hyperledger Indy Node documentation](https://hyperledger-indy.readthedocs.io/projects/node/en/latest/requests/) (ネットワークの操作方法)
* [Transactions — Hyperledger Indy Node documentation](https://hyperledger-indy.readthedocs.io/projects/node/en/latest/transactions/) (ネットワークで管理するデータ)

## ネットワークに接続する

ネットワークへの接続には、genesis ファイルという、最初に接続するべきノードの情報が書かれたファイルを用いる。
Indicio TestNet 用の genesis ファイルは、以下のページから取得できる。
[indicio-network/genesis_files/pool_transactions_testnet_genesis at main · Indicio-tech/indicio-network](https://github.com/Indicio-tech/indicio-network/blob/main/genesis_files/pool_transactions_testnet_genesis)
[`fetch()`](https://developer.mozilla.org/ja/docs/Web/API/Window/fetch) を用いて genesis ファイルのデータを取得し、これを [`PoolCreate`](https://github.com/hyperledger/indy-vdr/blob/main/wrappers/javascript/indy-vdr-shared/src/builder/PoolCreate.ts) に渡して pool を作成する。
この pool を用いることで、ネットワークにリクエストを送信できる。

## ネットワークの規約を取得する

Hyperledger Indy のネットワークの中には、データを書き込む際に [Transaction Author Agreement](https://github.com/hyperledger-indy/indy-sdk/blob/main/docs/how-tos/transaction-author-agreement.md) という規約に同意していなければならないものがあり、Indicio TestNet もその一つである。
[`GetTransactionAuthorAgreementRequest`](https://github.com/hyperledger/indy-vdr/blob/main/wrappers/javascript/indy-vdr-shared/src/builder/GetTransactionAuthorAgreementRequest.ts) リクエストを用いることで、この規約の内容を取得できる。
また、[`GetAcceptanceMechanismsRequest`](https://github.com/hyperledger/indy-vdr/blob/main/wrappers/javascript/indy-vdr-shared/src/builder/GetAcceptanceMechanismsRequest.ts) リクエストを用いることで、この規約にどのようにして同意するかの候補を取得できる。
これらのリクエストでは、データの書き込みは行わないため、署名や規約への同意は不要である。

それぞれ、以下のようなデータを取得できる。

```javascript:Indicio TestNet から取得した規約データ
{
  // 規約のバージョンと本文のハッシュ値
  digest: 'c965dd01fec099ea95babaea3031bc09905432d3d7f1519bc0b99971aece8592',
  // 規約の発効日時
  ratification_ts: 1606435200,
  // 規約の本文
  text: 'Indicio Transaction Author Agreement \n' +
    '\n' +
    'Version 1.3\n' +
    '\n' +
    (省略)
  // 規約のバージョン
  version: '1.3'
}
```

```javascript:Indicio TestNet から取得した同意方法
{
  // 同意方法の名前と説明
  aml: {
    at_submission: 'The agreement was reviewed by the user and accepted at the time of submission of this transaction.',
    for_session: 'The agreement was reviewed by the user and accepted at some point in the user’s session prior to submission.',
    on_file: 'An authorized person accepted the agreement, and such acceptance is on file with the user’s organization.',
    product_eula: 'The agreement was included in the software product’s terms and conditions as part of a license to the end user.',
    service_agreement: 'The agreement was included in the terms and conditions the user accepted as part of contracting a service.',
    wallet_agreement: 'The agreement was reviewed by the user and this affirmation was persisted in the user’s wallet for use during submission.'
  },
  // 同意方法に関するコンテキスト (省略可)
  amlContext: 'https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/TAA/AML.md',
  // 同意方法のバージョン
  version: '1.0'
}
```

## データを登録するリクエストを作成する

データを登録するリクエストを表す [`NymRequest`](https://github.com/hyperledger/indy-vdr/blob/main/wrappers/javascript/indy-vdr-shared/src/builder/NymRequest.ts) を作成する。
今回は、以下のオプションを指定する。

|オプション|内容|
|---|---|
|`submitterDid`|endorser のID|
|`dest`|登録するID|
|`verkey`|登録するIDに対応する公開鍵|
|`diddocContent`|登録するIDに関する追加情報|
|`version`|登録するIDと公開鍵の対応を表す数値|

`diddocContent` は、[Base DIDDoc Template](https://hyperledger.github.io/indy-did-method/#base-diddoc-template) に無い追加情報を表す JSON の文字列を指定する。
今回は、以下の情報を追加する。

* `verificationMethod` に、JWK 形式の鍵 ([`JsonWebKey2020`](https://w3c-ccg.github.io/lds-jws2020/)) を追加する
* `assertionMethod` に、追加した鍵のIDを登録する

## リクエストに規約への同意情報を付与する

リクエストを表すオブジェクトの `setTransactionAuthorAgreementAcceptance` 関数を用いて、リクエストの送信者が規約に同意していることを表す情報を付与する。
具体的には、以下の情報を指定する。

|メンバ名|内容|
|---|---|
|`mechanism`|規約に同意した方法|
|`taaDigest`|同意した規約のハッシュ値|
|`time`|規約に同意した日 (`86400` の倍数である UNIX 時間)|

## リクエストに署名を行う

今回は、endorser が自分をリクエストの送信元としてリクエストを行うので、自分の署名だけがあればよい。
[リクエストを表すオブジェクト](https://github.com/hyperledger/indy-vdr/blob/main/wrappers/javascript/indy-vdr-shared/src/indyVdr/IndyVdrRequest.ts) の `signatureInput` プロパティで署名対象のデータ (文字列) が得られる。
これに対し、[`crypto.sign`](https://nodejs.org/api/crypto.html#cryptosignalgorithm-data-key-callback) を用いて署名を生成する。
生成した署名を [`Uint8Array`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) にして、リクエストを表すオブジェクトの `setSignature` 関数で設定する。

:::note info
署名の対象には規約への同意情報も含まれるので、付与する場合は必ず規約への同意情報を**付与した後で**署名を行う。
:::

## リクエストを送信する

[`IndyVdrPool`](https://github.com/hyperledger/indy-vdr/blob/main/wrappers/javascript/indy-vdr-shared/src/indyVdr/IndyVdrPool.ts) (`PoolCreate` はこれを継承している) の `submitRequest` 関数にリクエストを表すオブジェクトを渡すことで、リクエストを送信できる。

## 実装

ここまでの手順を実行する、以下のコードを用意した。

```typescript:AssertionKeyRegisterer.mts
import {
  PoolCreate,
  NymRequest,
  GetAcceptanceMechanismsRequest,
  GetAcceptanceMechanismsResponse,
  GetTransactionAuthorAgreementRequest,
  GetTransactionAuthorAgreementResponse,
} from "@hyperledger/indy-vdr-nodejs";
import {
  sign,
  KeyObject,
} from "node:crypto";

export interface TaaAcceptance {
  digest: string;
  mechanism: string;
  acceptedDate: Date;
}

export class AssertionKeyRegisterer {
  private constructor(
    private pool: PoolCreate,
    private endorserId: string,
    private endorserPrivateKey: KeyObject,
  ) {
  }

  // DIDの登録を行うクラスを生成する
  static async create(
    genesisFileUrl: string,        // genesis ファイルのURL
    endorserId: string,            // endorser のネットワーク内ID
    endorserPrivateKey: KeyObject, // endorser の秘密鍵
  ): Promise<AssertionKeyRegisterer> {
    const genesisResponse = await fetch(genesisFileUrl);
    if (!genesisResponse.ok) {
      throw new Error("failed to fetch genesis file");
    }
    const genesisData = await genesisResponse.text();
    return new AssertionKeyRegisterer(
      new PoolCreate({
        parameters: {
          transactions: genesisData,
        },
      }),
      endorserId,
      endorserPrivateKey,
    );
  }

  // Transaction Author Agreement の情報を取得する
  async getTransactionAuthorAgreement(): Promise<GetTransactionAuthorAgreementResponse["result"]["data"]> {
    const taaRequest = new GetTransactionAuthorAgreementRequest({});
    const res = await this.pool.submitRequest(taaRequest);
    return res.result.data;
  }

  // Acceptance Mechanisms の情報を取得する
  async getAcceptanceMechanisms(): Promise<GetAcceptanceMechanismsResponse["result"]["data"]> {
    const amlRequest = new GetAcceptanceMechanismsRequest({});
    const res = await this.pool.submitRequest(amlRequest);
    return res.result.data;
  }

  // DIDを登録する
  async register(
    did: string,                                // 登録するDID (did:indy: などの部分を含む)
    verkey: string,                             // 登録するIDに対応する公開鍵
    assertionKey: KeyObject | null = null,      // 登録する署名検証用の鍵
    taaAcceptance: TaaAcceptance | null = null, // 規約への同意情報
    transactionVersion: 0 | 1 | 2 = 2,          // 用いるトランザクションのバージョン
  ): Promise<void> {
    // 登録するIDを取得する
    const lastComma = did.lastIndexOf(":");
    if (lastComma < 0) {
      throw new Error("invalid DID");
    }
    const idToRegister = did.substring(lastComma + 1);

    // 追加する署名検証用の鍵の情報を用意する
    const diddocContent = assertionKey ?
      JSON.stringify({
        "@context": [
          "https://www.w3.org/ns/did/v1",
          "https://w3id.org/security/suites/ed25519-2018/v1",
          "https://w3id.org/security/suites/jws-2020/v1",
        ],
        verificationMethod: [{
          id: did + "#assertionkey",
          type: "JsonWebKey2020",
          controller: did,
          publicKeyJwk: {
            ...assertionKey.export({format: "jwk"}),
            kid: "assertionkey",
          },
        }],
        assertionMethod: [
          did + "#assertionkey",
        ],
      }) : undefined;

    // ID登録のリクエストを作成する
    const nym = new NymRequest({
      submitterDid: this.endorserId,
      dest: idToRegister,
      verkey,
      diddocContent,
      version: transactionVersion,
    });

    // リクエストに規約への同意情報を付与する
    if (taaAcceptance) {
      nym.setTransactionAuthorAgreementAcceptance({
        acceptance: {
          mechanism: taaAcceptance.mechanism,
          taaDigest: taaAcceptance.digest,
          time: Math.floor(taaAcceptance.acceptedDate.getTime() / 86400000) * 86400,
        },
      });
    }

    // リクエストに署名をする
    const signature = sign(
      null,
      Buffer.from(nym.signatureInput),
      this.endorserPrivateKey,
    );
    nym.setSignature({
      signature: new Uint8Array(signature),
    });

    // リクエストを送信する
    await this.pool.submitRequest(nym);
  }
}
```

Node.js の対話型実行環境から、たとえば以下のようにして実行できる。
(秘密鍵は伏せている)

```javascript
const { generateKeyAndId } = await import("./generateKeyAndId.mjs")
const { AssertionKeyRegisterer } = await import("./AssertionKeyRegisterer.mjs")
const cr = require("node:crypto")
eprk = cr.createPrivateKey({
  key: {
    crv: 'Ed25519',
    d: '*******************************************',
    x: 'NsEGIJ2BJoNxyjWk365iMreA-lzUKN8jAO5MVGUv48Y',
    kty: 'OKP'
  },
  format: "jwk",
})
akr = await AssertionKeyRegisterer.create(
  "https://raw.githubusercontent.com/Indicio-tech/indicio-network/refs/heads/main/genesis_files/pool_transactions_testnet_genesis",
  "7m9sSPa5FbfvGgYMhoe4Ro",
  eprk
)
acc = {
  digest: "c965dd01fec099ea95babaea3031bc09905432d3d7f1519bc0b99971aece8592",
  mechanism: "for_session",
  acceptedDate: new Date(),
}
info = generateKeyAndId()
kp = cr.generateKeyPairSync("ec",{ namedCurve:"P-256" })
await akr.register(
  "did:indy:indicio:test:" + info.id,
  info.verkey,
  kp.publicKey,
  acc
)
```

今回は、これを用いて

```text
did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs
```

を登録した。

# 登録したデータを Credo で確認する

前章で追加の鍵を含むデータを登録したはずであるが、残念ながら執筆時点では [Universal Resolver](https://dev.uniresolver.io/) でこの追加情報を確認することはできないようである。
(生の `diddocContent` のデータの確認はできるが、DID DOCUMENT に反映されないようである)

そこで、[Credo](https://github.com/openwallet-foundation/credo-ts) という、~~TypeScript 用を謳っており、~~ DID の resolve ができるライブラリを用いて、登録したデータを確認する。

:::note info
よく見たら、「TypeScript で書かれている」というだけで、「TypeScript 用」とは謳っていないかもしれない。
:::

まず、Credo と関連ライブラリをインストールする。
Credo はバージョンアップによる変化が大きい可能性があるため、バージョンを指定してインストールする。

```shell
npm i @credo-ts/core@0.5 @credo-ts/node@0.5 \
      @credo-ts/indy-vdr@0.5 @hyperledger/indy-vdr-nodejs \
      @credo-ts/askar@0.5 @hyperledger/aries-askar-nodejs
```

そして、Credo で DID の resolve を行うための agent を作成する以下のコードを用意した。

```typescript:createCredoAgent.mts
import { Agent, DidsModule } from "@credo-ts/core";
import { agentDependencies } from "@credo-ts/node";
import { IndyVdrModule, IndyVdrPoolConfig, IndyVdrIndyDidResolver } from "@credo-ts/indy-vdr";
import { indyVdr } from "@hyperledger/indy-vdr-nodejs";
import { AskarModule } from "@credo-ts/askar";
import { ariesAskar } from "@hyperledger/aries-askar-nodejs";

// キーにネットワークの識別子、値に genesis ファイルのURLを指定する
export type GenesisFiles = Record<string, string>;

export async function createCredoAgent(genesisFiles: GenesisFiles): Promise<Agent> {
  const networks: IndyVdrPoolConfig[] = [];
  const genesisFilesEntries = Object.entries(genesisFiles);
  for (let i = 0; i < genesisFilesEntries.length; i++) {
    const [identifier, url] = genesisFilesEntries[i];
    const genesisResponse = await fetch(url);
    if (!genesisResponse.ok) {
      throw new Error("failed to fetch genesis file for " + identifier);
    }
    const genesisData = await genesisResponse.text();
    networks.push({
      isProduction: false,
      indyNamespace: identifier,
      genesisTransactions: genesisData,
      connectOnStartup: true,
    });
  }
  if (!networks[0]) {
    throw new Error("at least 1 network is required");
  }
  return new Agent({
    config: {
      label: "agent",
      walletConfig: {
        id: "placeholder",
        key: "placeholder",
      },
    },
    dependencies: agentDependencies,
    modules: {
      indyVdr: new IndyVdrModule({
        indyVdr,
        networks: networks as [IndyVdrPoolConfig, ...IndyVdrPoolConfig[]],
      }),
      askar: new AskarModule({
        ariesAskar,
      }),
      dids: new DidsModule({
        resolvers: [new IndyVdrIndyDidResolver()],
      }),
    },
  });
}
```

:::note warn
今回は、wallet を積極的に使わないので、`walletConfig.key` には適当な値をハードコードで設定している。
しかし、wallet に秘密鍵を保存するなど、wallet を活用する場合は、この値は複雑なものにし、またソースコードに埋め込まず環境変数などで指定するようにしたほうがよいと考えられる。
:::

`npx tsc` でトランスパイルを行うと、エラーが出てしまった。
エラーはライブラリ内部で発生しており、解決は難しそうである。
まあ JavaScript への変換はできており、動かせるので、一旦無視でいいか……

<details><summary>エラーメッセージ</summary>

```text
node_modules/@animo-id/mdoc/dist/index.d.ts:1:21 - error TS2307: Cannot find module 'jose' or its corresponding type declarations.

1 import { JWK } from 'jose';
                      ~~~~~~

node_modules/@animo-id/pex/dist/main/lib/types/Internal.types.d.ts:3:33 - error TS2307: Cannot find module '../validation/validators' or its corresponding type declarations.

3 import { ValidationError } from '../validation/validators';
                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/@credo-ts/core/build/agent/AgentDependencies.d.ts:3:13 - error TS1259: Module '"/work/node_modules/@types/ws/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag

3 import type WebSocket from 'ws';
              ~~~~~~~~~

  node_modules/@types/ws/index.d.ts:445:1
    445 export = WebSocket;
        ~~~~~~~~~~~~~~~~~~~
    This module is declared with 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag.

node_modules/@credo-ts/core/build/error/BaseError.d.ts:26:8 - error TS1259: Module '"/work/node_modules/make-error/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag

26 import makeError from 'make-error';
          ~~~~~~~~~

  node_modules/make-error/index.d.ts:47:1
    47 export = makeError;
       ~~~~~~~~~~~~~~~~~~~
    This module is declared with 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag.

node_modules/@credo-ts/node/build/transport/WsInboundTransport.d.ts:2:8 - error TS1259: Module '"/work/node_modules/@types/ws/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag

2 import WebSocket, { Server } from 'ws';
         ~~~~~~~~~

  node_modules/@types/ws/index.d.ts:445:1
    445 export = WebSocket;
        ~~~~~~~~~~~~~~~~~~~
    This module is declared with 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag.

node_modules/@sphereon/kmp-mdl-mdoc/@sphereon/kmp-mdl-mdoc.d.ts:842:11 - error TS2417: Class static side 'typeof StringLabel' incorrectly extends base class static side 'typeof CoseLabel'.
  Types of property 'Static' are incompatible.
    Property 'fromCborItem' is missing in type '{}' but required in type '{ fromCborItem<ItemType extends unknown>(cborItem: CborItem<ItemType>): CoseLabel<any>; }'.

842     class StringLabel extends com.sphereon.cbor.CoseLabel<string> {
              ~~~~~~~~~~~

  node_modules/@sphereon/kmp-mdl-mdoc/@sphereon/kmp-mdl-mdoc.d.ts:743:13
    743             fromCborItem<ItemType extends any>(cborItem: com.sphereon.cbor.CborItem<ItemType>): com.sphereon.cbor.CoseLabel<any/* kotlin.Comparable<UnknownType *> */>;
                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    'fromCborItem' is declared here.

node_modules/@sphereon/kmp-mdl-mdoc/@sphereon/kmp-mdl-mdoc.d.ts:1907:27 - error TS2304: Cannot find name 'IJwkJson'.

1907             fromJson(jwk: IJwkJson): com.sphereon.crypto.jose.Jwk;
                               ~~~~~~~~

node_modules/@sphereon/ssi-types/dist/mapper/credential-mapper.d.ts:1:28 - error TS2307: Cannot find module '@veramo/core' or its corresponding type declarations.

1 import { IssuerType } from '@veramo/core';
                             ~~~~~~~~~~~~~~


Found 8 errors in 7 files.

Errors  Files
     1  node_modules/@animo-id/mdoc/dist/index.d.ts:1
     1  node_modules/@animo-id/pex/dist/main/lib/types/Internal.types.d.ts:3
     1  node_modules/@credo-ts/core/build/agent/AgentDependencies.d.ts:3
     1  node_modules/@credo-ts/core/build/error/BaseError.d.ts:26
     1  node_modules/@credo-ts/node/build/transport/WsInboundTransport.d.ts:2
     2  node_modules/@sphereon/kmp-mdl-mdoc/@sphereon/kmp-mdl-mdoc.d.ts:842
     1  node_modules/@sphereon/ssi-types/dist/mapper/credential-mapper.d.ts:1
```

</details>

agent を作成したら、`initialize()` で初期化を行ったあと、`dids.resolve()` で DID の resolve を行う。
今回は、Node.js の対話型実行環境でデータを省略させず、見やすくするため、JSONに変換して出力するようにした。

```javascript
const { createCredoAgent } = await import("./createCredoAgent.mjs")
const agent = await createCredoAgent({
  "indicio:test": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/refs/heads/main/genesis_files/pool_transactions_testnet_genesis",
})
await agent.initialize()
console.log(JSON.stringify(
  await agent.dids.resolve("did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs"),
  null, 2))
```

以下の結果が得られた。
登録した追加情報が取得できている。

```json
{
  "didDocument": {
    "@context": [
      "https://w3id.org/did/v1",
      "https://w3id.org/security/suites/ed25519-2018/v1",
      "https://www.w3.org/ns/did/v1",
      "https://w3id.org/security/suites/jws-2020/v1"
    ],
    "id": "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs",
    "verificationMethod": [
      {
        "id": "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs#verkey",
        "type": "Ed25519VerificationKey2018",
        "controller": "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs",
        "publicKeyBase58": "DpMhLRuApikkuMFzPKtAitHeqxdyEggCAVNskPwcRfjW"
      },
      {
        "controller": "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs",
        "id": "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs#assertionkey",
        "publicKeyJwk": {
          "crv": "P-256",
          "kid": "assertionkey",
          "kty": "EC",
          "x": "x_Nn-nhreCE7Lf8gFiMi85Gr68X-HSocnfUdNEiPxcQ",
          "y": "Bp4WAf0zEiOETN3IAEMmN1h_4e9EQ5YcEseXkWDcn8w"
        },
        "type": "JsonWebKey2020"
      }
    ],
    "authentication": [
      "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs#verkey"
    ],
    "assertionMethod": [
      "did:indy:indicio:test:LcCxTQ7svE1XvRUBCfebxs#assertionkey"
    ]
  },
  "didDocumentMetadata": {},
  "didResolutionMetadata": {
    "contentType": "application/did+ld+json",
    "resolutionTime": 1750485474774,
    "servedFromCache": true
  }
}
```

# まとめ

1. Ed25519 の鍵ペアを生成し、公開鍵のデータを Base58 エンコードすることで verkey を作成した
2. verkey から、[Indicio TestNet](https://indyscan.indiciotech.io/home/IND_TESTNET3) の Endorser (書き込み承認者) 登録フォームで受け入れられるIDを作成した
3. ID と verkey を登録フォームで Indicio TestNet の Endorser として登録した
4. [Indy VDR NodeJS](https://www.npmjs.com/package/@hyperledger/indy-vdr-nodejs) を用いて、Indicio TestNet に接続した
5. Indicio TestNet から規約情報を取得した
6. Indicio TestNet に `assertionMethod` を含むデータを登録するためのリクエストを構築 (Endorser の鍵による署名を含む) し、送信することで登録を行った
7. 登録した DID の resolve を [Credo](https://github.com/openwallet-foundation/credo-ts) を用いて行い、登録した情報が取得できることを確認した

Indicio TestNet の Endorser 登録フォームでは、登録する DID と Verkey の組み合わせに**何らかの制約**があると推測できる。
**隠された謎の制約**を満たしていないと登録できなそうだが、この謎の制約を満たさなかったらしいときに「must be a 32 byte base58 encoded string」という**既に満たしている制約**しか開示されないことがあり、**不親切である。**
実験の結果、DID の Base58 デコード結果と、Verkey の Base58 デコード結果の最初の16バイトを一致させる (NYM トランザクション バージョン 1 の仕様) ことで、登録ができる可能性を上げることができそうである。
なお、フォームで入力した DID を登録するトランザクションにおいて、バージョンは指定されていない (すなわち、DID と Verkey の関係に制約はない) ようであった。

冒頭で「本当は JavaScript でいいと思う」と書いたが、実装を行ってみると、ライブラリに渡すデータの間違い (オブジェクトのメンバとして渡すべきデータを、オブジェクトに入れずに直接渡すなど) や識別子の typo といったミスをしてしまった。
JavaScript では、このようなミスの発見は、

1. 実行する
2. うまく動かないことに気付く
3. ミスを探す (デバッグ)

という流れになるだろう。
一方、TypeScript では、これらのミスをトランスパイル時に発見し、具体的な場所を指摘してくれるので、実行する前にミスに気付いてすぐに修正できた。
そのため、TypeScript を使う意義は大いにあったと考えられる。