1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

credo-tsで「Email Verification Service」からemail VCを取得してみる(その2)

Posted at

はじめに

前回Open Wallet Foundationで開発が行われているcredo-tsを用いて、sphereon社のVC issuerデモサイトからナレッジやスキルを証明するOpen Badge VC(Verifiable Credential)の取得をやってみた。

このときVC発行に使用されていたプロトコルはOIDC4VCIなのだが、業界を見渡すと、これ以外のプロトコルとして「DIDComm」「ISO23220」等があるそうだ(引用:identiverse 2023)。

layer.png

DIDCommでVC発行をしているデモサイトとしてはBritish Columbia州が運営している「Email Verification Service」あるようで、サイトにemailを登録すると、そのemail宛てにurlが送られてきて、そのurlをクリックするとQRが提示されemail VCが取得できる。

credo-tsはもともとHL-ariesの中で開発されてきたので、DIDCommとは相性がいいはずである。credo-tsのどこに何を書けば「Email Verification Service」とDIDCommでの会話がはじまるのかを中心に調査をしてみた。

DIDcommで接続するための設定

1. getOpenIdHolderModules()の内容

前回、baseAgentを継承してOIDC4VCに対応したholder classを作ったときgetOpenIdHolderModules()を介していろいろOIDC4VCI関連のモジュールを設定したのだが、DIDComm通信可能にするにあたっては、この関数が返す値に、DIDComm関連のものを追加する必要がある。

// SphereonのOIDC4VCと接続可能なgetOpenIdHolderModules()
... <snip>...
  public constructor(port: number, name: string) {
    super({ port, name, modules: getOpenIdHolderModules() })
  }
... <snip>...

  function getOpenIdHolderModules() {
  return {
    askar: new AskarModule({ ariesAskar }),
    openId4VcHolder: new OpenId4VcHolderModule(),// <---ここ
  } as const
}
... <snip>...

 具体的には、下記のようにする。DIDComm関連で読み込まないといけないものは、DIDCommデモコード:credo-ts/demo/src/BaseAgent.tsの内容をそのままコピーしたものであるが、ただコピーしただけでは動作しなかったのでいくつか変更を加える必要がある。変更した点は2点で、1つには今まで動いていた「OIDC4VCIが動かなくなってしまった」や、「Email Verification Serviceと接続できない」への対処部分である。

  • 「OIDC4VCIが動かなくなってしまった」への対処

    • dids: new DidsModule({resolvers: [..., new JwkDidResolver()],..})とjwkをresolveできるjwk resolverを追加することで解決
  • 「Email Verification Serviceと接続できない」への対処

    • email verification serviceが公開鍵を公開しているのは、sovring:stagingなので、その接続情報をindyVdr: new IndyVdrModule({indyVdr,networks: [indyNetworkConfig],})のindyNetworkConfigに設定することで解決。indyNetworkConfigは、bifold-walletのledgers.jsonから取得
    • mediatorを追加. DIDCommはお互いがpublic アドレスを前提としているので、wallet部分をlocalhostなどで動かすと、issuer側からのDIDComm通信が届かない。その中継をしてくれるのがmediator。詳細はcredo-ts doc-mediationに。
      • getOpenIdHolderModules()に、mediationRecipient: new MediationRecipientModule({mediatorInvitationUrl,}),を追加
      • mediatorInvitationUrlは、bifold-walletで利用されている"https://public.mediator.indiciotech.io?c_i=eyJAd..<snip>..F0b3IifQ=="を利用

これらの対策をいれたgetOpenIdHolderModules()が下記で、これによって「Email Verification Service」とDIDCommで通信可能になる。


//Email Verification ServiceのDIDComm/SphereonのOIDC4VCと接続可能なgetOpenIdHolderModules()

import _ledgers from './ledgers.json'
const ledgers: IndyVdrPoolConfig[] = _ledgers

const mediatorInvitationUrl ="https://public.mediator.indiciotech.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiMDVlYzM5NDItYTEyOS00YWE3LWEzZDQtYTJmNDgwYzNjZThhIiwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwczovL3B1YmxpYy5tZWRpYXRvci5pbmRpY2lvdGVjaC5pbyIsICJyZWNpcGllbnRLZXlzIjogWyJDc2dIQVpxSktuWlRmc3h0MmRIR3JjN3U2M3ljeFlEZ25RdEZMeFhpeDIzYiJdLCAibGFiZWwiOiAiSW5kaWNpbyBQdWJsaWMgTWVkaWF0b3IifQ==";
export const indyNetworkConfig = getNetworkConfig('sovring:staging') satisfies IndyVdrPoolConfig

function getNetworkConfig(name: string): IndyVdrPoolConfig{
  let ret= ledgers[0];
  ledgers.forEach((element: IndyVdrPoolConfig)=>{
    if (element.indyNamespace == name){
       ret = element;
    }
  })
  return ret;
}

function getOpenIdHolderModules() {
  const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService()
  const legacyIndyProofFormatService = new LegacyIndyProofFormatService()

  return {
    connections: new ConnectionsModule({
      autoAcceptConnections: true,
    }),
    credentials: new CredentialsModule({
      autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
      credentialProtocols: [
        new V1CredentialProtocol({
          indyCredentialFormat: legacyIndyCredentialFormatService,
        }),
        new V2CredentialProtocol({
          credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()],
        }),
      ],
    }),
    anoncreds: new AnonCredsModule({
      registries: [new IndyVdrAnonCredsRegistry(), new CheqdAnonCredsRegistry()],
      anoncreds,
    }),
    //ポイント1:issuerが属しているnetwork「sovring:staging」を指定
    indyVdr: new IndyVdrModule({
      indyVdr,
      networks: [indyNetworkConfig],
    }),
    // ポイント2:使用するmediationを設定
    mediationRecipient: new MediationRecipientModule({
      mediatorInvitationUrl,
    }),
    // ポイント3:使用するDIDに対応するresolverをここに登録
    dids: new DidsModule({
      resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver(), new JwkDidResolver()],
      registrars: [new CheqdDidRegistrar(), new JwkDidRegistrar()],
    }),
    askar: new AskarModule({
      ariesAskar,
    }),
    // OIDC4VCIのモジュール
    openId4VcHolder: new OpenId4VcHolderModule(),
  } as const
}

2. walletへのDIDComm実装

walletの動きもOIDC4VCの時とは異なる。OIDC4VCの場合はTLS接続の中でやりとりされるが、DIDCommの場合は、DIDComm接続の中でやりとりされる。DIDComm接続では前述したようにpublic IPをもつmediatorを介して通信が行われる。walletとしてはmediatorからの応答をlistenする実装になる。

didcomm.png

2.1 OIDC4VCの場合

参考までに、credo-tsを用いたOIDC4VCへのVC要求&取得は、下記のようになる。

async function getCredentialOid4vc(_initiationURI:string){

      // --> Issuerのmeta情報を取得
      const resolvedCredentialOffer 
         = await wallet.holder.resolveCredentialOffer(initiationURI); 

      // --> tokenを取得して、VCを取得
      const credentials = await wallet.holder.requestAndStoreCredentials(
         resolvedCredentialOffer,
         resolvedCredentialOffer.offeredCredentials.map((o) => o.id)
      )
     // --> credentialを抽出
     let decoded={}
     if (credentials[0].type === 'W3cCredentialRecord') {
        decoded = credentials[0].credential.jsonCredential;
    } 

    return {result:true, detail:"",decode:decoded};
}

2.2 DIDCommの場合

一方、credo-tsを用いたDIDComm接続でのVC要求&取得は、下記のようになる。


async function getCredentialDidcomm(_initiationURI:string){
  return new Promise(async (resolve, reject) => {
    await wallet.holder.acceptConnection(_initiationURI)
    if (!wallet.holder.connectionRecordFaberId) reject(1)

    console.log("start listening..")
    wallet.holder.agent.events.on( CredentialEventTypes.CredentialStateChanged, 
      async ({ payload }: CredentialStateChangedEvent) => {
        console.log("============1====================")
        console.log(JSON.stringify(payload, null,2))
        console.log("============1====================")
        if (payload.credentialRecord.state === CredentialState.OfferReceived) {
          // --> VCを要求
          await newCredentialPrompt(payload.credentialRecord)
        }
        if (payload.credentialRecord.state === "done"){
           // --> 取得したVC
           resolve(payload.credentialRecord)
        }
      }
    );
  });
}

async function newCredentialPrompt(credentialRecord: CredentialExchangeRecord) {
    await wallet.holder.acceptCredentialOffer(credentialRecord)
}

取得してみよう

1. credo-tsで作ったwalletコードを配置

 wallet.ts, Holder.ts, BaseAgent.ts, OutputClass.ts、package.json, index.html, qr-scanner.min.js, qr-scanner-worker.min.jsを以下のフォルダ構成のように配置する.

├── wallet.ts
├── Holder.ts
├── BaseAgent.ts
├── OutputClass.ts (*1)
├── tsconfig.json
├── package.json
└── views/
   ├── index.html
   ├── qr-scanner.min.js (*2)
   ├── qr-scanner-worker.min.js (*2)

(*1)は、https://github.com/openwallet-foundation/credo-ts/tree/main/demo-openid/src からdownloadする。
(*2)は、https://github.com/nimiq/qr-scanner からdownloadする。

wallet.tsのコードを見る
wallet.ts

import {
  CredentialEventTypes,
  CredentialState,
  CredentialExchangeRecord,
  CredentialStateChangedEvent,
  ProofEventTypes,
  ProofExchangeRecord,
  ProofStateChangedEvent,
  ProofState,
} from '@credo-ts/core'
import { Holder } from './Holder'
import { Title, greenText, redText, Color, purpleText  } from './OutputClass'
import { textSync } from 'figlet'

import  express from 'express';
import bodyParser from 'body-parser';
import * as path from "path";
import timeout from 'connect-timeout';

//************
const port=8800;
let wallet:Agent
let credential="";
//*************

const app: express.Express = express();
app.use('/', express.static(path.join(process.cwd(), 'views')));
app.use(bodyParser.json());
//app.use(timeout('60s'))

function delay(ms: number) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms)
    });
}

app.listen(port, async () => {
       console.log(textSync('Holder', { horizontalLayout: 'full' }))
       wallet = await Agent.build()
       console.log(`\n web page start: listening on port ${port}`);

});


app.post('/.*', async function(req:express.Request,res:express.Response){
	console.log("\n====POST====");
	console.log("cmd:"+JSON.stringify(req.params));
	console.log("query:"+JSON.stringify(req.query));
	console.log("headers:"+JSON.stringify(req.headers));
	console.log(req.body);
	console.log("====POST====");

  let cmd = req.params[0].split('/');
  if (cmd[0] =="issuer_init"){
      console.log("init")

      if (req.body.qr.indexOf("?c_i=")!==-1){
        try {
          let cred = await getCredentialDidcomm(req.body.qr)
          console.log("start callback..")
          console.log(JSON.stringify({result:true, detail:"Anoncreds",decode:cred}))
          res.send({"result":true,"detail":"Anoncreds","decode":"b262c0ac-3fe5-414e-abca-2c554606af6e"})
//          res.send({"result":true, "detail":"Anoncreds","decode":cred});
       }catch(e){
          console.log(e);
       }
      }else{
        let cred =  await getCredentialOid4vc(req.body.qr)
        res.send(cred);
     }
//  await delay(60000)
//  res.send({"result":true,"detail":"Anoncreds","decode":"b262c0ac-3fe5-414e-abca-2c554606af6e"})
  }
})

async function getCredentialDidcomm(_initiationURI:string){
  return new Promise(async (resolve, reject) => {
    await wallet.holder.acceptConnection(_initiationURI)
    if (!wallet.holder.connectionRecordFaberId) reject(1)
    console.log(wallet.holder.connectionRecordFaberId)

    console.log("start listening..")
    wallet.holder.agent.events.on( CredentialEventTypes.CredentialStateChanged, 
      async ({ payload }: CredentialStateChangedEvent) => {
        console.log("============1====================")
        console.log(JSON.stringify(payload, null,2))
        console.log("============1====================")
        if (payload.credentialRecord.state === CredentialState.OfferReceived) {
          console.log("OfferReceived")
          await newCredentialPrompt(payload.credentialRecord)
        }
        if (payload.credentialRecord.state === "done"){
           console.log("finish")
           printCredentialAttributes(payload.credentialRecord)
           credential=JSON.stringify(payload, null,2)
           console.log("return to caller")
           resolve(payload.credentialRecord.id)
        }
      }
   );
   wallet.holder.agent.events.on(ProofEventTypes.ProofStateChanged, 
     async ({ payload }: ProofStateChangedEvent) => {
        console.log("============2====================")
        console.log(JSON.stringify(payload, null,2))
        console.log("============2====================")
       if (payload.proofRecord.state === ProofState.RequestReceived) {
          await newProofRequestPrompt(payload.proofRecord)
       }
    })
  });
}

async function newCredentialPrompt(credentialRecord: CredentialExchangeRecord) {
    console.log("newCredentialPrompt")
    await wallet.holder.acceptCredentialOffer(credentialRecord)
}

async function newProofRequestPrompt(proofRecord: ProofExchangeRecord) {
    console.log("newProofRequestPrompt")
    await wallet.holder.acceptProofRequest(proofRecord)
  }

function printCredentialAttributes(credentialRecord: CredentialExchangeRecord) {
    console.log("printCredentialAttributes")
    if (credentialRecord.credentialAttributes) {
      const attribute = credentialRecord.credentialAttributes
      console.log('\n\nCredential preview:')
      attribute.forEach((element) => {
        console.log(purpleText(`${element.name} ${Color.Reset}${element.value}`))
      })
    }
}

async function getCredentialOid4vc(_initiationURI:string){
      let initiationURI = _initiationURI.replace("OpenBadgeCredential", "OpenBadgeCredentialJwt")

      const resolvedCredentialOffer = await wallet.holder.resolveCredentialOffer(initiationURI);
      console.log(greenText(`Received credential offer for the following credentials.`))
      console.log(greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.id).join('\n')))

      const credentials = await wallet.holder.requestAndStoreCredentials(
         resolvedCredentialOffer,
        resolvedCredentialOffer.offeredCredentials.map((o) => o.id)
      )
      console.log(credentials)
      let decoded={}
     if (credentials[0].type === 'W3cCredentialRecord') {
        console.log(greenText(`W3cCredentialRecord with claim format ${credentials[0].credential.claimFormat}`, true))
        console.log(JSON.stringify(credentials[0].credential.jsonCredential, null, 2))
        decoded = credentials[0].credential.jsonCredential;
    } 

    return {result:true, detail:"",decode:decoded};
}

export class Agent {
  public holder: Holder

  public constructor(holder: Holder) {
    this.holder = holder
  }

  public static async build(): Promise<Agent> {
    const holder = await Holder.build()
    return new Agent(holder)
  }
}

Holder.tsのコードを見る
Holder.ts

import type { ConnectionRecord, CredentialExchangeRecord, ProofExchangeRecord } from '@credo-ts/core'
import type { OpenId4VciResolvedCredentialOffer, OpenId4VcSiopResolvedAuthorizationRequest } from '@credo-ts/openid4vc'
import type { IndyVdrPoolConfig } from '@credo-ts/indy-vdr'

import {
  AnonCredsCredentialFormatService,
  AnonCredsModule,
  AnonCredsProofFormatService,
  LegacyIndyCredentialFormatService,
  LegacyIndyProofFormatService,
  V1CredentialProtocol,
  V1ProofProtocol,
} from '@credo-ts/anoncreds'
import { AskarModule } from '@credo-ts/askar'
import {
  CheqdAnonCredsRegistry,
  CheqdDidRegistrar,
  CheqdDidResolver,
  CheqdModule,
  CheqdModuleConfig,
} from '@credo-ts/cheqd'

import {
  ConnectionsModule,
  DidsModule,
  V2ProofProtocol,
  V2CredentialProtocol,
  ProofsModule,
  AutoAcceptProof,
  AutoAcceptCredential,
  CredentialsModule,
  W3cJwtVerifiableCredential,
  W3cJsonLdVerifiableCredential,
  DifPresentationExchangeService,
  JwkDidResolver,
  JwkDidRegistrar,
  MediationRecipientModule,
} from '@credo-ts/core'
import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@credo-ts/indy-vdr'
import { OpenId4VcHolderModule } from '@credo-ts/openid4vc'
import { anoncreds } from '@hyperledger/anoncreds-nodejs'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
import { indyVdr } from '@hyperledger/indy-vdr-nodejs'

import { BaseAgent } from './BaseAgent'
import { greenText, Output, redText } from './OutputClass'

// https://github.com/openwallet-foundation/bifold-wallet/blob/main/packages/legacy/core/App/configs/ledgers/indy/ledgers.json
import _ledgers from './ledgers.json'
const ledgers: IndyVdrPoolConfig[] = _ledgers

// https://github.com/openwallet-foundation/bifold-wallet/blob/7dbf2fe36051bec685a170bfa7441042b589384b/DEVELOPER.md
const mediatorInvitationUrl ="https://public.mediator.indiciotech.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiMDVlYzM5NDItYTEyOS00YWE3LWEzZDQtYTJmNDgwYzNjZThhIiwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwczovL3B1YmxpYy5tZWRpYXRvci5pbmRpY2lvdGVjaC5pbyIsICJyZWNpcGllbnRLZXlzIjogWyJDc2dIQVpxSktuWlRmc3h0MmRIR3JjN3U2M3ljeFlEZ25RdEZMeFhpeDIzYiJdLCAibGFiZWwiOiAiSW5kaWNpbyBQdWJsaWMgTWVkaWF0b3IifQ==";


export const indyNetworkConfig = getNetworkConfig('bcovrin:test') satisfies IndyVdrPoolConfig
export const indyNetworkConfig2 = getNetworkConfig('sovring:staging') satisfies IndyVdrPoolConfig

function getNetworkConfig(name: string): IndyVdrPoolConfig{
  let ret= ledgers[0];
  ledgers.forEach((element: IndyVdrPoolConfig)=>{
    if (element.indyNamespace == name){
       ret = element;
    }
  })
  return ret;
}

function getOpenIdHolderModules() {
  const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService()
  const legacyIndyProofFormatService = new LegacyIndyProofFormatService()

  return {
    connections: new ConnectionsModule({
      autoAcceptConnections: true,
    }),
    credentials: new CredentialsModule({
      autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
      credentialProtocols: [
        new V1CredentialProtocol({
          indyCredentialFormat: legacyIndyCredentialFormatService,
        }),
        new V2CredentialProtocol({
          credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()],
        }),
      ],
    }),
    proofs: new ProofsModule({
      autoAcceptProofs: AutoAcceptProof.ContentApproved,
      proofProtocols: [
        new V1ProofProtocol({
          indyProofFormat: legacyIndyProofFormatService,
        }),
        new V2ProofProtocol({
          proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService()],
        }),
      ],
    }),
    anoncreds: new AnonCredsModule({
      registries: [new IndyVdrAnonCredsRegistry(), new CheqdAnonCredsRegistry()],
      anoncreds,
    }),
    indyVdr: new IndyVdrModule({
      indyVdr,
      networks: [indyNetworkConfig2],
    }),
    cheqd: new CheqdModule(
      new CheqdModuleConfig({
        networks: [
          {
            network: 'testnet',
            cosmosPayerSeed:
              'robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt',
          },
        ],
      })
    ),
    dids: new DidsModule({
      resolvers: [new IndyVdrIndyDidResolver(), new CheqdDidResolver(), new JwkDidResolver()],
      registrars: [new CheqdDidRegistrar(), new JwkDidRegistrar()],
    }),
    askar: new AskarModule({
      ariesAskar,
    }),
    mediationRecipient: new MediationRecipientModule({
      mediatorInvitationUrl,
    }),
    openId4VcHolder: new OpenId4VcHolderModule(),
  } as const
}



export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>> {
  public connected: boolean
  public connectionRecordFaberId?: string

  public constructor(port: number, name: string) {
    super({ port, name, modules: getOpenIdHolderModules() })
    this.connected = false
  }

  public static async build(): Promise<Holder> {
    const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString())
    await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e')
    return holder
  }

  public async resolveCredentialOffer(credentialOffer: string) {
    return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer)
  }

  public async requestAndStoreCredentials(
    resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer,
    credentialsToRequest: string[]
  ) {
    const credentials = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(
      resolvedCredentialOffer,
      {
        credentialsToRequest,
        // TODO: add jwk support for holder binding
        credentialBindingResolver: async () => ({
          method: 'did',
          didUrl: this.verificationMethod.id,
        }),
      }
    )

    const storedCredentials = await Promise.all(
      credentials.map((credential) => {
        if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) {
          return this.agent.w3cCredentials.storeCredential({ credential })
        } else {
          return this.agent.sdJwtVc.store(credential.compact)
        }
      })
    )

    return storedCredentials
  }

  public async resolveProofRequest(proofRequest: string) {
    const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(proofRequest)

    return resolvedProofRequest
  }

  public async acceptPresentationRequest(resolvedPresentationRequest: OpenId4VcSiopResolvedAuthorizationRequest) {
    const presentationExchangeService = this.agent.dependencyManager.resolve(DifPresentationExchangeService)

    if (!resolvedPresentationRequest.presentationExchange) {
      throw new Error('Missing presentation exchange on resolved authorization request')
    }

    const submissionResult = await this.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({
      authorizationRequest: resolvedPresentationRequest.authorizationRequest,
      presentationExchange: {
        credentials: presentationExchangeService.selectCredentialsForRequest(
          resolvedPresentationRequest.presentationExchange.credentialsForRequest
        ),
      },
    })

    return submissionResult.serverResponse
  }

  public async acceptConnection(invitation_url: string) {
    const connectionRecord = await this.receiveConnectionRequest(invitation_url)
    this.connectionRecordFaberId = await this.waitForConnection(connectionRecord)
    return this.connectionRecordFaberId
  }

  public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) {
    console.log("acceptCredentialOffer called:"+credentialRecord.id)
    await this.agent.credentials.acceptOffer({
      credentialRecordId: credentialRecord.id,
    })
    
  }

  public async acceptProofRequest(proofRecord: ProofExchangeRecord) {
    const requestedCredentials = await this.agent.proofs.selectCredentialsForRequest({
      proofRecordId: proofRecord.id,
    })

    await this.agent.proofs.acceptRequest({
      proofRecordId: proofRecord.id,
      proofFormats: requestedCredentials.proofFormats,
    })
    console.log(greenText('\nProof request accepted!\n'))
  }

  public async sendMessage(message: string) {
    const connectionRecord = await this.getConnectionRecord()
    await this.agent.basicMessages.sendMessage(connectionRecord.id, message)
  }

  private async getConnectionRecord() {
    if (!this.connectionRecordFaberId) {
      throw Error(redText(Output.MissingConnectionRecord))
    }
    return await this.agent.connections.getById(this.connectionRecordFaberId)
  }

  private async receiveConnectionRequest(invitationUrl: string) {
    const { connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl)
    if (!connectionRecord) {
      throw new Error(redText(Output.NoConnectionRecordFromOutOfBand))
    }
    return connectionRecord
  }

  private async waitForConnection(connectionRecord: ConnectionRecord) {
    connectionRecord = await this.agent.connections.returnWhenIsConnected(connectionRecord.id)
    this.connected = true
    console.log(greenText(Output.ConnectionEstablished))
    return connectionRecord.id
  }
}
BaseAgent.tsのコードを見る
BaseAgent.ts

import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod} from '@credo-ts/core'
import type { Express } from 'express'

import { Agent, DidKey, HttpOutboundTransport, KeyType, TypedArrayEncoder , WsOutboundTransport } from '@credo-ts/core'
import { HttpInboundTransport, agentDependencies} from '@credo-ts/node'
import express from 'express'

import { greenText } from './OutputClass'

export class BaseAgent<AgentModules extends ModulesMap> {
  public app: Express
  public port: number
  public name: string
  public config: InitConfig
  public agent: Agent<AgentModules>
  public did!: string
  public didKey!: DidKey
  public kid!: string
  public verificationMethod!: VerificationMethod

  // 読み込んだ引数に従って、Agentに設定する名前や、読み込むmodulesを設定
  public constructor({ port, name, modules }: { port: number; name: string; modules: AgentModules }) {
    this.name = name
    this.port = port
    this.app = express()

    const config = {
      label: name,
      walletConfig: { id: name, key: name }
    } satisfies InitConfig

    this.config = config

    this.agent = new Agent({ config, dependencies: agentDependencies, modules })

    const httpInboundTransport = new HttpInboundTransport({ app: this.app, port: this.port })
    const httpOutboundTransport = new HttpOutboundTransport()

    this.agent.registerOutboundTransport(new WsOutboundTransport())
    this.agent.registerInboundTransport(httpInboundTransport)
    this.agent.registerOutboundTransport(httpOutboundTransport)
  }
  // constructotで設定された値どおりにagentを初期化。引数にsecretPrivateKeyをもち、その値でdid keyを生成し、agentに設定する
  public async initializeAgent(secretPrivateKey: string) {
    await this.agent.initialize()

    const didCreateResult = await this.agent.dids.create<KeyDidCreateOptions>({
      method: 'key',
      options: { keyType: KeyType.Ed25519 },
      secret: { privateKey: TypedArrayEncoder.fromString(secretPrivateKey) },
    })

    this.did = didCreateResult.didState.did as string
    this.didKey = DidKey.fromDid(this.did)
    this.kid = `${this.did}#${this.didKey.key.fingerprint}`

    const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(this.kid, ['authentication'])
    if (!verificationMethod) throw new Error('No verification method found')
    this.verificationMethod = verificationMethod

    console.log(greenText(`\nAgent ${this.name} created!\n`))
  }
}
index.htmlのコードを見る
index.html

<!DOCTYPE html>
<html lang='en'>

<head><meta charset='utf-8'></head>
<body>
  <div align=center>
    <div id="title">[ID Wallet]</div><br>
    <button id="scanQR">scan QR code</button>, <button id="stop-button">Stop</button>
    <p><div id=msg></div>
    <p><div id=qr></div>
    <div id="video-container">
      <video id="qr-video"></video>
    </div>
  </div>
<b>Detected QR code: </b>
<span id="cam-qr-result">None</span>
<div id="credresult"></div>
<br>
<hr></body>
<script type="module">
    import QrScanner from "./qr-scanner.min.js";

    let credentials =[]
    const video = document.getElementById('qr-video');
    const videoContainer = document.getElementById('video-container');
    const camQrResult = document.getElementById('cam-qr-result');
    const credResult = document.getElementById('credresult');

    function setResult(label, result) {
        console.log(result.data);
        scanner.stop();
        label.textContent = result.data;
        label.style.color = 'teal';
        _init(result.data)
    }

    const scanner = new QrScanner(video, result => setResult(camQrResult, result), {
        onDecodeError: error => {
            camQrResult.textContent = error;
            camQrResult.style.color = 'inherit';
        },
        highlightScanRegion: true,
        highlightCodeOutline: true,
    });

    document.getElementById('scanQR').addEventListener('click', () => {
        scanner.start();
    });


    function _init(_qrcode) {
      let setting={
        qr:_qrcode
      }
      sendData("./.issuer_init", JSON.stringify(setting), 'application/json', 
        function(data){
          try {
            data = JSON.parse(data)
            if (data.result){
              credentials.push({detail:data.detail, decode:data.decode})
              credResult.innerHTML = "<pre>"+JSON.stringify(credentials[credentials.length-1].decode, null,"\t")+"</pre>";
            }else{
              credResult.innerHTML = "<br>--> failed<br>";
            }
          }catch(e){
              credResult.innerHTML = "<br>--> "+e;
          }
        }
      );
    }

    // for debugging
    window.scanner = scanner;

    document.getElementById('stop-button').addEventListener('click', () => {
        scanner.stop();
    });


</script>
<script type="text/javascript">
  window.addEventListener('load',function() {  
  });

  function getData(url, type, cb){
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
      switch(xhr.readyState){
        case 4:
          if (xhr.status == 200||xhr.status ==304){
            cb(xhr.response)
          }
      }
    };
    xhr.open("GET", url, false);
    xhr.setRequestHeader('Content-Type', type);
    xhr.send('');
  }

  function sendData(url, data, type, cb){
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
      switch(xhr.readyState){
        case 4:
          if (xhr.status == 200||xhr.status ==304){
            console.log("send success");
            cb(xhr.response)
          }
      }
    };
    xhr.open("POST", url, false);
    xhr.setRequestHeader('Content-Type', type);
    xhr.send(data);
  }
  </script>
</html>
package.jsonのコードを見る
package.json

{
  "name": "agent-credo-ts",
  "version": "1.0.0",
  "scripts": {
    "start": "ts-node wallet.ts",
    "stop": "rm -rf ./node_modules ./yarn.lock && yarn"
  },
  "dependencies": {
    "@hyperledger/anoncreds-nodejs": "^0.2.1",
    "@hyperledger/aries-askar-nodejs": "^0.2.0",
    "@hyperledger/indy-vdr-nodejs": "^0.2.0",
    "body-parser": "^1.20.2",
    "express": "^4.18.1"
  },
  "devDependencies": {
    "@credo-ts/anoncreds": "*",
    "@credo-ts/askar": "*",
    "@credo-ts/cheqd": "*",
    "@credo-ts/core": "*",
    "@credo-ts/indy-vdr": "*",
    "@credo-ts/node": "*",
    "@credo-ts/openid4vc": "*",
    "@types/express": "^4.17.13",
    "@types/figlet": "^1.5.4",
    "@types/inquirer": "^8.2.6",
    "figlet": "^1.5.2",
    "ts-node": "^10.4.0"
  }
}

tsconfig.jsonのコードを見る
tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
    // ... other options
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}

2. 関連モジュールをインストールする

(注) 動作確認した環境は、nodejs(v18.18.0)/WSL(Ubuntu-20.04.6)/windows 10

> yarn
...<snip>...
? Please choose a version of "@credo-ts/askar" from this list: (Use arrow keys)
❯ 0.5.0-alpha.138
  0.5.0-alpha.137
  0.5.0-alpha.136
  0.5.0-alpha.135
  0.5.0-alpha.134
  0.5.0-alpha.133
  0.5.0-alpha.132
  0.5.0-alpha.131
  0.5.0-alpha.130

インストール途中、上記のようにversion選択画面が何度かでてくる。動作確認は最新版の0.5.0-alpha.138で行っている

3. wallet にスマホからアクセスするための名前を割当

> ngrok http 8800
Forwarding       https://fe86-153-214-37-xxx.ngrok-free.app -> http://localhost:8800 

4. credo-tsで作ったwallet を起動

環境によって起動に時間がかかる。筆者の環境だと5分程度

> ts-node wallet.ts
  _   _           _       _
 | | | |   ___   | |   __| |   ___   _ __
 | |_| |  / _ \  | |  / _` |  / _ \ | '__|
 |  _  | | (_) | | | | (_| | |  __/ | |
 |_| |_|  \___/  |_|  \__,_|  \___| |_|


Agent OpenId4VcHolder 0.7639780978890356 created!


 web page start: listening on port 8800

5. スマホブラウザでhttps://fe86-153-214-37-xxx.ngrok-free.app/にアクセス

  1. BCのEmail verification Serviceのデモサイトにアクセスし、QRコードを表示
    bc.png

  2. スマホブラウザの「scan QR code」ボタンを押して、デモサイトのQRコードを読み取ると、デモサイトと通信が始まる
    mediator.png

  3. スマホで読み取ったQRコードは、credo-tsで作ったwalletに送られ、処理される。そのDIDComm通信のやりとりは以下。

====POST====
init

Connection established!
2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a
start listening..
============1====================
{
  "credentialRecord": {
    "_tags": {},
    "metadata": {},
    "credentials": [],
    "id": "c8e7c7c3-3691-474f-ba92-edf6b428067b",
    "createdAt": "2024-03-10T07:22:53.230Z",
    "state": "offer-received",
    "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
    "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7",
    "protocolVersion": "v1",
    "updatedAt": "2024-03-10T07:22:53.250Z"
  },
  "previousState": null
}
============1====================
OfferReceived
newCredentialPrompt
acceptCredentialOffer called:c8e7c7c3-3691-474f-ba92-edf6b428067b
============1====================
{
  "credentialRecord": {
    "_tags": {
      "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
      "state": "offer-received",
      "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7"
    },
    "metadata": {
      "_anoncreds/credentialRequest": {
        "link_secret_blinding_data": {
          "v_prime": "13093366292574770601024868982378262308643194712464088037693835031008443488809221855025896482373979777485493057161906298014522684908230282962248947636487678590032951956506483350238173878780490993172498798993734226068522087906134373718748921425839956873458650173278086363326736317132398458513930828468162259459919327836562708985660560806571513577470061929984134975434096692064996836560182715065293087191612984452191087770532889917503054389921731654460490017558428738793734548876458088820933863017516681957594114398489556013297433445493613740439716818598940439771522976804107239852740992832806899016475435468950600347107351866993732704270192110",
          "vr_prime": null
        },
        "nonce": "281326680474692413578803",
        "link_secret_name": "ed46714f-c425-42ab-b39f-39f0cc06efa3"
      },
      "_anoncreds/credential": {
        "credentialDefinitionId": "MTYqmTBoLT7KLP5RNfgK3b:3:CL:160342:default",
        "schemaId": "MTYqmTBoLT7KLP5RNfgK3b:2:verified-email:1.2.3"
      }
    },
    "credentials": [],
    "id": "c8e7c7c3-3691-474f-ba92-edf6b428067b",
    "createdAt": "2024-03-10T07:22:53.230Z",
    "state": "request-sent",
    "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
    "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7",
    "protocolVersion": "v1",
    "updatedAt": "2024-03-10T07:23:05.538Z",
    "credentialAttributes": [
      {
        "mime-type": "text/plain",
        "name": "email",
        "value": "xxxxxxx@gmail.com"
      },
      {
        "mime-type": "text/plain",
        "name": "time",
        "value": "2024-03-10 07:22:50.902325"
      }
    ]
  },
  "previousState": "offer-received"
}
============1====================
============1====================
{
  "credentialRecord": {
    "_tags": {
      "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
      "state": "request-sent",
      "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7"
    },
    "metadata": {
      "_anoncreds/credentialRequest": {
        "link_secret_blinding_data": {
          "v_prime": "13093366292574770601024868982378262308643194712464088037693835031008443488809221855025896482373979777485493057161906298014522684908230282962248947636487678590032951956506483350238173878780490993172498798993734226068522087906134373718748921425839956873458650173278086363326736317132398458513930828468162259459919327836562708985660560806571513577470061929984134975434096692064996836560182715065293087191612984452191087770532889917503054389921731654460490017558428738793734548876458088820933863017516681957594114398489556013297433445493613740439716818598940439771522976804107239852740992832806899016475435468950600347107351866993732704270192110",
          "vr_prime": null
        },
        "nonce": "281326680474692413578803",
        "link_secret_name": "ed46714f-c425-42ab-b39f-39f0cc06efa3"
      },
      "_anoncreds/credential": {
        "credentialDefinitionId": "MTYqmTBoLT7KLP5RNfgK3b:3:CL:160342:default",
        "schemaId": "MTYqmTBoLT7KLP5RNfgK3b:2:verified-email:1.2.3"
      }
    },
    "credentials": [
      {
        "credentialRecordType": "anoncreds",
        "credentialRecordId": "0a07a545-e307-4bb5-a054-7ca072f277e2"
      }
    ],
    "id": "c8e7c7c3-3691-474f-ba92-edf6b428067b",
    "createdAt": "2024-03-10T07:22:53.230Z",
    "state": "credential-received",
    "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
    "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7",
    "protocolVersion": "v1",
    "updatedAt": "2024-03-10T07:23:18.342Z",
    "credentialAttributes": [
      {
        "mime-type": "text/plain",
        "name": "email",
        "value": "xxxxxxx@gmail.com"
      },
      {
        "mime-type": "text/plain",
        "name": "time",
        "value": "2024-03-10 07:22:50.902325"
      }
    ]
  },
  "previousState": "request-sent"
}
============1====================
============1====================
{
  "credentialRecord": {
    "_tags": {
      "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
      "state": "request-sent",
      "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7"
    },
    "metadata": {
      "_anoncreds/credentialRequest": {
        "link_secret_blinding_data": {
          "v_prime": "13093366292574770601024868982378262308643194712464088037693835031008443488809221855025896482373979777485493057161906298014522684908230282962248947636487678590032951956506483350238173878780490993172498798993734226068522087906134373718748921425839956873458650173278086363326736317132398458513930828468162259459919327836562708985660560806571513577470061929984134975434096692064996836560182715065293087191612984452191087770532889917503054389921731654460490017558428738793734548876458088820933863017516681957594114398489556013297433445493613740439716818598940439771522976804107239852740992832806899016475435468950600347107351866993732704270192110",
          "vr_prime": null
        },
        "nonce": "281326680474692413578803",
        "link_secret_name": "ed46714f-c425-42ab-b39f-39f0cc06efa3"
      },
      "_anoncreds/credential": {
        "credentialDefinitionId": "MTYqmTBoLT7KLP5RNfgK3b:3:CL:160342:default",
        "schemaId": "MTYqmTBoLT7KLP5RNfgK3b:2:verified-email:1.2.3"
      }
    },
    "credentials": [
      {
        "credentialRecordType": "anoncreds",
        "credentialRecordId": "0a07a545-e307-4bb5-a054-7ca072f277e2"
      }
    ],
    "id": "c8e7c7c3-3691-474f-ba92-edf6b428067b",
    "createdAt": "2024-03-10T07:22:53.230Z",
    "state": "done",
    "connectionId": "2e13a5c0-735e-4320-b9a9-c13ddf3f1a2a",
    "threadId": "c4f19a5e-6ef7-4418-b4a1-9ee0ce8b1ed7",
    "protocolVersion": "v1",
    "updatedAt": "2024-03-10T07:23:18.375Z",
    "credentialAttributes": [
      {
        "mime-type": "text/plain",
        "name": "email",
        "value": "xxx@gmail.com"
      },
      {
        "mime-type": "text/plain",
        "name": "time",
        "value": "2024-03-10 07:22:50.902325"
      }
    ]
  },
  "previousState": "credential-received"
}
============1====================

参考文献

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?