EthereumでDID/VCを実装してみる

PS/SLの佐々木です。

今回はEthereumを使用して簡単なDID/VCを実現してみようと思います。

DID/VCとは

DID/VCとは分散IDと検証可能なデジタル証明書のことを指します。

通常の認証とは異なり、認証(VCを検証)する際に発行元に問い合わせる必要がありません。

よって発行元がなくなってしまっても問題なく検証ができます。

また検証もDID Resolverを使用することで誰でも検証することができるため、特定の機関が発行した証明書(VC)を他サービスで検証することも容易です。

DIDとEthereumの関係性

ブロックチェーンの技術はDID/VCの文脈で相性が良くたびたび話題に出ますが、一体どこでブロックチェーンが使用されているのでしょうか?

DID/VCの文脈でブロックチェーンが登場するときには検証用の公開鍵の保存先としてブロックチェーンが登場します。

例えば今回実装を紹介するEthereumではEIP1056という規格で規定されています。

よってVCの中身の情報であったり実際の検証に関してはブロックチェーンが登場することはありません。

ではなぜブロックチェーンと相性が良いのでしょうか?

それはどちらも分散型である点が挙げられます。

例えばdid:webの場合特定のドメインに依存する形でDIDを解決をする必要があります。一方でdid:ethrを使用する場合には分散ネットワーク上にあるため世界中のどこの誰であってもアクセスすることができるためDID/VCの世界観により近いと言えます。

DID/VCの実装にあたって必要な予備知識

今回はタイトル通りdid:ethrを使用してDID/VCを実現します。

Ethereumで実現するためにはEIP1056というDIDを管理する標準的な仕様があります。

ERC1056は、W3Cが提案するDID仕様と互換性があり、Ethereumエコシステムの中で自己主権型IDを管理するための標準的な手段となっています。

このERC1056を実装するためのライブラリ、コントラクトとして以下のようなものが提供されており、今回はこれらに倣って実装していきます。

  • ethr-did-registry
    • 公開鍵を保存したり、did-documentに表示される属性を更新できるようにするEthereumのコントラクトコードです。
    • これらは自分でデプロイしてそこに公開鍵やその他属性を追加してもよいですが、ここに各ネットワークにデプロイされているコントラクトがあるのでそれを利用しても問題ないです。(今回は事前にデプロイされているSepoliaネットワークのコントラクトを使用します)
  • ethr-did-resolver
    • 与えられたdidメソッド(did:ethr:development:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74)をもとにDIDドキュメントを解決したり、VCを検証したりし、DIDドキュメントを返します。
  • ethr-did
    • DIDの作成や更新をするために使用します。

実装するユースケース

大学の証明書を発行しそれを検証するようなものを想定します。

今回はKYC認証は行わないのでVCを提出しているユーザーがVCの本人かどうかは保証しません。あくまでVCが改ざんされていないかを検証するところのみになります。

実装手順

  1. DIDの登録
    1. 大学が検証用の公開鍵をEthereum に登録します。
  2. VCの発行
    1. 大学が証明書を発行
  3. VCを検証

準備

実装

今回実装するソースコードはこちらで公開しています。

フロントエンド:React

API:Nest.js

どちらも簡単なコードで作りこみはしていません。

フロントエンドはフォームとレスポンスをわかりやすく作っているだけなので今回はAPIのapp.service.ts を見ていきます。(app.controller.ts はルーティングしかしてません)

事前に紹介した三つの機能についてそれぞれ実装を紹介します。

DID登録

import { Injectable } from '@nestjs/common';
import { Resolver } from 'did-resolver';
import { getResolver } from 'ethr-did-resolver';
import { VerifiedCredential, verifyCredential } from 'did-jwt-vc';
import { EthrDID, KeyPair } from 'ethr-did';
import { ethers, Wallet } from 'ethers';
  async registerDID(): Promise<void> {
    const alchemyApiKey = process.env.ALCHEMY_API_KEY!!;

    const privateKey = process.env.PRIVATE_KEY!!;
    const wallet = new Wallet(privateKey);
    const pubkey = wallet.signingKey.publicKey;
    const address = wallet.address;

    const provider = new ethers.AlchemyProvider('sepolia', alchemyApiKey);
    const txSigner = new Wallet(privateKey, provider);

    const keypair: KeyPair = {
      privateKey,
      publicKey: pubkey,
      address,
      identifier: pubkey,
    };
    // ユーザーのDID作成
    const ethrDid = new EthrDID({
      ...keypair,
      provider: provider,
      txSigner: txSigner, // こいつを渡さないと失敗する <https://github.com/uport-project/ethr-did/issues/81#issuecomment-1030181286>
      chainNameOrId: 'sepolia',
      registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818',
    });
    // DID登録
    await ethrDid.setAttribute(
      'did/pub/Secp256k1/sigAuth/hex',
      pubkey,
      31104000,
    );
  }

ここでは検証用に使用する公開鍵をEtheruemに登録する処理を行っています。

登録するスマートコントラクトはすでにライブラリ開発者がでプロしてくれているsepoliaのスマートコントラクトをレジストリとして使用させてもらいます。0x03d5003bf0e79C5F5223588F347ebA39AfbC3818

Ethereumに登録するトランザクションはこれだけなので公開鍵の登録が終わったらガス代がこれ以降かかることはありません。

DID作成の時には公開鍵、秘密鍵、EOAアドレス、AlchemyProvider、Wallet、デプロイ先のチェーン、スマートコントラクトのアドレスを渡します。

const keypair: KeyPair = {
  privateKey,
  publicKey: pubkey,
  address,
  identifier: pubkey,
};
// ユーザーのDID作成
const ethrDid = new EthrDID({
  ...keypair,
  provider: provider,
  txSigner: txSigner, // こいつを渡さないと失敗する <https://github.com/uport-project/ethr-did/issues/81#issuecomment-1030181286>
  chainNameOrId: 'sepolia',
  registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818',
});

そして公開鍵の登録の際には公開鍵の形式と用途を指定して登録します。

登録方法はこのようになっており、今回は以下のような意味を持っています・

  • DIDの公開鍵であること。
  • Secp256k1曲線に基づいた公開鍵暗号アルゴリズムを使用していること。
  • 署名と認証のために使用される公開鍵であること。
  • 公開鍵の値は16進数形式で表現されていること。
// DID登録
await ethrDid.setAttribute(
  'did/pub/Secp256k1/sigAuth/hex',
  pubkey,
  31104000,
);

 

VCの発行

 async issueVc(
    holderAddress: string,
    type: string,
    name: string,
  ): Promise<string> {
    const vcpayload = {
      '@context': ['<https://www.w3.org/2018/credentials/v1>'],
      type: ['VerifiableCredential'],
      issuer: 'did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8',
      issuanceDate: new Date().toISOString(),
      credentialSubject: {
        id: `did:ethr:sepolia:${holderAddress}`,
        degree: {
          type: type,
          name: name,
        },
      },
      proof: {
        type: 'EcdsaSecp256k1Signature2019',
        created: new Date().toISOString(),
        proofPurpose: 'assertionMethod',
        verificationMethod:
          'did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8#delegate-1',
      },
    };

    const alchemyApiKey = process.env.ALCHEMY_API_KEY!!;
    const privateKey = process.env.PRIVATE_KEY!!;
    const wallet = new Wallet(privateKey);
    const pubkey = wallet.signingKey.publicKey;
    const address = wallet.address;

    const provider = new ethers.AlchemyProvider('sepolia', alchemyApiKey);
    const txSigner = new Wallet(privateKey, provider);

    const keypair: KeyPair = {
      privateKey,
      publicKey: pubkey,
      address,
      identifier: address,
    };
    // ユーザーのDID作成
    const ethrDid = new EthrDID({
      ...keypair,
      provider: provider,
      txSigner: txSigner, // こいつを渡さないと失敗する <https://github.com/uport-project/ethr-did/issues/81#issuecomment-1030181286>
      chainNameOrId: 'sepolia',
      registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818',
    });

    const vc = await ethrDid.signJWT(vcpayload);
    return vc;
  }

VCにもW3Cが定めている規格が存在しています。

https://www.w3.org/TR/vc-data-model-2.0/

今回は実装に焦点を当てるため詳細な説明は行いませんが、発行したいVCに合うような形でVCを作成する必要があります。

以下は今回使用するVCの簡単な紹介です。

  • @context
    • VCが準拠する標準的なデータ構造の文脈を示します。
    • ['<https://www.w3.org/2018/credentials/v1>']はW3Cが定めたVerifiable Credentialsの標準的な構造に従うことを示します。
  • type
    • VCの種類を示すプロパティです。
    • ['VerifiableCredential']は、この証明書が検証可能な証明書であることを明示しています。
  • issuer
    • 証明書の発行者(Issuer)のDIDを示します。
    • この場合、発行者はEthereumのSepoliaテストネット上にあるdid:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8というDIDを持つエンティティです。
  • issuanceDate
    • この証明書が発行された日付を示します。
    • new Date().toISOString()を使って現在の日時をISO 8601形式で自動的に生成します。
  • credentialSubject
    • 証明書が関連する主体(Credential Subject)を定義します。このVCに記載される情報の対象です。
    • id: did:ethr:sepolia:${hodlerAddress}では、証明書の対象となる人物や組織のDIDを示します。この部分はhodlerAddressという変数で動的に設定されるEthereumのアドレスです。
    • degreeフィールドは証明書の内容を示しています。具体的には「学位」情報として、type(学位の種類)とname(学位の名称)が含まれています。これらも変数typenameによって動的に設定される情報です。
  • proof
    • このVCがどのように署名され、検証されるかを示す情報です。
    • type: EcdsaSecp256k1Signature2019は、Secp256k1楕円曲線を使ったECDSA(楕円曲線デジタル署名アルゴリズム)による署名方式を指定しています。これはEthereumなどのブロックチェーンで広く使われている暗号方式です。
    • created: 署名が生成された日時です。ここでもnew Date().toISOString()が使われ、現在の日時が設定されます。
    • proofPurpose: この署名が何のために行われるのかを示します。assertionMethodは、発行者が証明書の内容を主張するために行った署名であることを意味します。
    • verificationMethod: 証明書の署名を検証するための公開鍵や検証方法を指定しています。この場合、did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8#delegate-1が指定されています。この公開鍵は#delegate-1という識別子で特定されています。

またVCはJWTトークンとして返却されます。

const vc = await ethrDid.signJWT(vcpayload);

例えば以下のような内容でVCを作成しようとすると

以下のようなトークンが返ってきます。

これがVCになります。

eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NkstUiJ9.eyJpYXQiOjE3Mjk1OTE2ODcsIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmV0aHI6c2Vwb2xpYToweDE0NTI0MjI4NkFFODE4NGNBODg1RTZCMTM0RTFBMWJBNzM4NThCRTgiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTEwLTIyVDEwOjA4OjA3LjUwNFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpldGhyOnNlcG9saWE6MHhFNDk1RkEwMmI3N0I3ZmVFMzVBMDQ2ODYwNDhEMDg0NzY1MTczMThFIiwiZGVncmVlIjp7InR5cGUiOiLmg4XloLHlt6XlraYiLCJuYW1lIjoi5bel5a2mIn19LCJwcm9vZiI6eyJ0eXBlIjoiRWNkc2FTZWNwMjU2azFTaWduYXR1cmUyMDE5IiwiY3JlYXRlZCI6IjIwMjQtMTAtMjJUMTA6MDg6MDcuNTA0WiIsInByb29mUHVycG9zZSI6ImFzc2VydGlvbk1ldGhvZCIsInZlcmlmaWNhdGlvbk1ldGhvZCI6ImRpZDpldGhyOnNlcG9saWE6MHgxNDUyNDIyODZBRTgxODRjQTg4NUU2QjEzNEUxQTFiQTczODU4QkU4I2RlbGVnYXRlLTEifSwiaXNzIjoiZGlkOmV0aHI6c2Vwb2xpYToweDE0NTI0MjI4NkFFODE4NGNBODg1RTZCMTM0RTFBMWJBNzM4NThCRTgifQ.nMBiW1Yi5vyghoorBRAwMIFHTRfATn_P3hv0ZF_aeeUowti7epUep90ldsuubkylccAmjlIiZwGcB9SgCd5IWAA

 

 VCの検証

最後に先ほど取得したVCを検証してみます。

async verifyVc(vc: string): Promise<VerifiedCredential | boolean> {
    const providerConfig = {
      // While experimenting, you can set a rpc endpoint to be used by the web3 provider
      rpcUrl: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
      // You can also set the address for your own ethr-did-registry (ERC1056) contract
      registry: '0x03d5003bf0e79C5F5223588F347ebA39AfbC3818',
      name: 'sepolia', // this becomes did:ethr:development:0x...
    };

    // It's recommended to use the multi-network configuration when using this in production
    // since that allows you to resolve on multiple public and private networks at the same time.

    // getResolver will return an object with a key/value pair of { "ethr": resolver } where resolver is a function used by the generic did resolver.
    const ethrDidResolver = getResolver(providerConfig);
    const didResolver = new Resolver(ethrDidResolver);
    try {
      const result = await verifyCredential(vc, didResolver);
      return result;
    } catch (error) {
      return false;
    }
  }

DIDリゾルバーを使用して発行元のDIDドキュメントを取得し、ドキュメントから公開鍵を取得しユーザーから提供されたVCを検証します。

DIDリゾルバーはDIDメソッドごとに異なります。

DIDメソッドを管理している団体がDIDリゾルバを提供していることが多いようです。今回はERC1056で紹介されていたDIDリゾルバを使用します。

DIDの解決から検証までは以下のコードになります。

  const ethrDidResolver = getResolver(providerConfig);
  const didResolver = new Resolver(ethrDidResolver);
 
  const result = await verifyCredential(vc, didResolver);

提供されているライブラリが優秀すぎて何をしているのかよくわからないので順に解説していきます。

まず最初の2行でDIDリゾルバーの設定を行います。

これはネットワークやリゾルバが参照するコントラクトのアドレスを設定します。

最後の1行でDIDを解決するところからVCの検証まで一括して行っています。

DIDの解決では先ほどのJWTトークンの中にあるissuerというプロパティのDIDを解決します。

参考までに先ほどのJWTトークンをデコードしてみます。

{
  "iat": 1729591687,
  "@context": [
    "<https://www.w3.org/2018/credentials/v1>"
  ],
  "type": [
    "VerifiableCredential"
  ],
  "issuer": "did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8",
  "issuanceDate": "2024-10-22T10:08:07.504Z",
  "credentialSubject": {
    "id": "did:ethr:sepolia:0xE495FA02b77B7feE35A04686048D08476517318E",
    "degree": {
      "type": "情報工学",
      "name": "工学"
    }
  },
  "proof": {
    "type": "EcdsaSecp256k1Signature2019",
    "created": "2024-10-22T10:08:07.504Z",
    "proofPurpose": "assertionMethod",
    "verificationMethod": "did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8#delegate-1"
  },
  "iss": "did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8"
}

これを見るとdid:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8 このDIDを解決していることがわかります。

続いてこのDIDを解決してみましょう。

DID解決を行ってくれるUniversal Resolverというサイトがあります。

ここで先ほどのDIDを入力すると以下のような結果になります。

これを見ると以下の公開鍵で検証すればよいことがわかります。

今回検証に使うのはproof.verificationMethod に指定されているため04a48ac40eade831fa352e56aaaab5396cf7f87627913543cf6d1dff5f5c0b496da62401ea4b0629fb3370d56855b2cf32d333e5c1bd0e53cf53722187d7eb3ff1

こちらの公開鍵を使用すればよいことがわかります。

  "proof": {
    "type": "EcdsaSecp256k1Signature2019",
    "created": "2024-10-22T10:08:07.504Z",
    "proofPurpose": "assertionMethod",
    "verificationMethod": "did:ethr:sepolia:0x145242286AE8184cA885E6B134E1A1bA73858BE8#delegate-1"
  },

実際に今回のサンプルで検証してみるとvefified:trueと帰ってくるので検証に成功していることがわかります。

最後の検証の部分で渡しているものはVCのみで発行者の情報などはVCからすべて取得できているため発行者に検証に関する問い合わせは行っていないことがわかります。

終わりに

今回はEthereumを使用したDID/VCを実装してみました。

DID/VCを手軽に実装している記事が少ないため実装にはかなり苦労しましたが、動くものを見ると具体的なイメージがわきやすいですね。

DID/VCについてはまだまだ不勉強な部分も多く、デファクトスタンダードと呼ばれるものもないため今後の動向に注目しつつ、今回説明できなかったDID/VCの細かい部分も今後紹介できたらと思います。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です