credo-tsでOID4VCを体験する(その１)


## はじめに
　さまざまなところでwallet開発が行われているが、その中でもLinux FoundationがすすめるOpen Wallet Foundation(OWF)はさまざまなプロトコルが合流しインターオペラビリティが図られるので魅力的だ。この団体は2023/2に設立され、その開発方向性を示すホワイトペーパ：「[今なぜ世界がオープンソースのデジタルウォレットを必要としているか](https://www.linuxfoundation.jp/publications/2023/04/open-wallet-foundation_jp/)」を出している。

>どのデバイス、どのオペレーティング システム、どのアプリやサービス、どの通貨にも対応し、誰でも使用できるポータブルで安全なウォレットの開発を支援するために設立

　そして2023/12に、Hyperledger配下だったAFJ(Aries Framework JavaScript)がOWFに合流し、[credo project](https://github.com/openwallet-foundation/credo-ts)が発足した。AFJがもつanonCreds, DIDComm, OID4VC, JSON-LD,..がOWFの中に入り、いままでOWFアーキだけしか見えなかったのが、動くものとして見えるようになってきている。(個人的には、１つの鍵でOID4VC, DIDCommそれぞれがもつユースケースをさばけるようなid walletを試してみたい..。)

https://github.com/openwallet-foundation/credo-ts/discussions/1586

　今日はこのcredo-tsを用いてwalletを作り、sphereonのOID4VCデモサイトにつなげてVCが取得できるところまでを試してみたいと思う。　試す前に少しcredo-tsのソフトウェア構成を覗いてみようと思う。

## credo-tsの中身
 credo-tsを用いたdemoプログラムは、[credo-ts/demo-openid/](https://github.com/openwallet-foundation/credo-ts/tree/main/demo-openid) にまとめられている。動きだけを確認するのであれば、readmeに書かれているように、issuer/holder/verifierをそれぞれ起動して確認することができる。ここでは、sphereon社が作ったOID4VC issuerデモサイトにつなげて、credo-tsの動きを見ていきたいと思う。
### 1. baseAgent
  ベースになっているクラスである。Agentがもつ鍵生成と、Agentが話すプロトコルに応じたmodule群を読み込む作業を担ってくれる。
``` baseAgent.ts
import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod } from '@credo-ts/core'
... <snip> ...

export class BaseAgent<AgentModules extends ModulesMap> {
... <snip> ...

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

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

    this.agent = new Agent({ config, dependencies: agentDependencies, modules })
    ...<snip>...
  }
  // 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
    ...<snip>...    
  }
}
```
### 2. Holder
　Holderは、さきほどのBAeAgentを拡張してOID4VC仕様でやりとりができるよういくつかの関数が+αされている。付加された関数は```@credo-ts/openid4vc```に定義されているもので、主にofferを分析する関数と、token/credential取得する関数である。
```Holder.ts
import { AskarModule } from '@credo-ts/askar'
import { BaseAgent } from './BaseAgent'
import { OpenId4VcHolderModule } from '@credo-ts/openid4vc'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
...<snip>...

// agentに読みこむモジュール
function getOpenIdHolderModules() {
  return {
    askar: new AskarModule({ ariesAskar }),
    openId4VcHolder: new OpenId4VcHolderModule(),
  } as const
}

// baseAgentを拡張して、Holderを定義
export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>> {
  public constructor(port: number, name: string) {
    super({ port, name, modules: getOpenIdHolderModules() })
  }

  // BaseAgentを呼び出して、agentを初期化。
  public static async build(): Promise<Holder> {
    const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString())
    await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e')
    return holder
  }

　// OID4VCI仕様で決められるofferを処理する関数
  public async resolveCredentialOffer(credentialOffer: string) {
    return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer)
  }

  // OID4VC仕様に従って、issuerからtokenをとり、その後credentialを取得する関数
  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
  }
... <snip> ...
}

```

### 3. wallet
さきほど説明したHolderを使って、OID4VC仕様のQRコードを処理するには、以下のような順番で関数を呼んでいけばOKである。
```typescript
// offerを処理。token/credential issuerと通信するためのendopointなどを算出(※)
const resolvedCredentialOffer = await wallet.holder.resolveCredentialOffer(initiationURI);

// 処理されたresolvedCredentialOfferを用いて、credentialsを取得
const credentials = await wallet.holder.requestAndStoreCredentials(
   resolvedCredentialOffer,
   resolvedCredentialOffer.offeredCredentials.map((o) => o.id)
)

// 取得されたcredentialを表示。
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))
} 

```
(※)credo-tsのresolveCredentialOffer関数は、offerに含まれる```"credentials":["OpenBadgeCredential"]```が、issuer metadataの中のcredentials_supported/idの中に含まれているかどうかを調べる。なので```credentials_supported[{..id:"OpenBadgeCredentialJwt",...},{}]```と不一致だとエラーになる。

## 体験してみよう
### 1. credo-tsで作ったwalletコードを配置
　server.ts, Holder.ts, BaseAgent.ts, OutputClass.ts、package.json, index.html, qr-scanner.min.js, qr-scanner-worker.min.jsを以下のフォルダ構成のように配置する.
```
├── server.ts
├── Holder.ts  (*1)
├── BaseAgent.ts (*1)
├── OutputClass.ts (*1)
├── 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する。

<details>

<summary>server.tsのコードを見る</summary>

```server.ts
import { Holder } from './Holder'
import { Title, greenText, redText } from './OutputClass'
import { textSync } from 'figlet'

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

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

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


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 ret="";
  let cmd = req.params[0].split('/');
  if (cmd[0] =="issuer_init"){
      console.log("init")
      let cred =  await getCredential(req.body.qr)
      res.send(cred);
  }
})

async function getCredential(_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:credentials,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)
  }
}

```

</details>

<details>

<summary>package.jsonのコードを見る</summary>

```package.json
{
  "name": "agent-credo-ts",
  "version": "1.0.0",
  "scripts": {
    "start": "ts-node server.ts",
    "stop": "rm -rf ./node_modules ./yarn.lock && yarn"
  },
  "dependencies": {
    "@hyperledger/anoncreds-nodejs": "^0.2.0",
    "@hyperledger/aries-askar-nodejs": "^0.2.0",
    "@hyperledger/indy-vdr-nodejs": "^0.2.0",
    "body-parser": "^1.20.2",
    "express": "^4.18.1",
    "inquirer": "^8.2.5"
  },
  "devDependencies": {
    "@credo-ts/askar": "*",
    "@credo-ts/core": "*",
    "@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"
  }
}

```
</details>
<details>

<summary>index.htmlのコードを見る</summary>

```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){
          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>";
          }
        }
      );
    }

    // 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>
```
</details>

### 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 server.ts
  _   _           _       _
 | | | |   ___   | |   __| |   ___   _ __
 | |_| |  / _ \  | |  / _` |  / _ \ | '__|
 |  _  | | (_) | | | | (_| | |  __/ | |
 |_| |_|  \___/  |_|  \__,_|  \___| |_|


Agent OpenId4VcHolder 0.7639780978890356 created!


 web page start: listening on port 8800
 ```
### 5. スマホブラウザで```https://fe86-153-214-37-xxx.ngrok-free.app/```にアクセス
1. sphereonの[OID4VCのデモサイト](https://ssi.sphereon.com/demo/issuer/)にアクセスし、QRコードを表示![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/d77dff3c-d621-84dc-ab61-3d6d8a444e24.png)

1. スマホブラウザの「scan QR code」ボタンを押して、デモサイトのQRコードを読み取る![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/4027a631-4648-46ae-4875-d296ad2d0708.png)
1. 読み取ったQRコードは、credo-tsで作ったwalletに送られ、処理される。
```
====POST====
cmd:{"0":"issuer_init" }
query:{}
headers:{"host":"fe86-153-214-37-xxx.ngrok-free","user-agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/122.0.6261.51 Mobile/15E148 Safari/604.1","content-length":"372","accept":"*/*","accept-encoding":"gzip, deflate, br","accept-language":"ja","content-type":"application/json","origin":"https://fe86-153-214-37-xxx.ngrok-free","referer":"https://fe86-153-214-37-xxx.ngrok-free/","x-forwarded-for":"xxx.xx.xx.xxx","x-forwarded-host":"fe86-153-214-37-xxx.ngrok-free","x-forwarded-proto":"https"}
{
  qr: 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22iD4dqVcNaCerMHpSeFF9AM%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22OpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fssi.sphereon.com%2Fpf3%22%7D'
}
====POST====
init
Received credential offer for the following credentials.
OpenBadgeCredentialJwt
[
  W3cCredentialRecord {
    _tags: { expandedTypes: [] },
    type: 'W3cCredentialRecord',
    metadata: Metadata { data: {} },
    id: '8e513c01-b6dc-474f-9048-649457f54240',
    createdAt: 2024-02-18T03:09:31.343Z,
    credential: W3cJwtVerifiableCredential {
      jwt: [Jwt],
      _credential: [W3cCredential]
    },
    updatedAt: 2024-02-18T03:09:31.343Z
  }
]
W3cCredentialRecord with claim format jwt_vc
```
最終的に、OID4VCプロトコルで取得されたVC(Verifiable Credential)はブラウザに送り返され、下記のようにスマホ上でみることができる。
![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/5fdc0f68-a82a-97f9-e019-6bb62f001e26.png)

## 参考記事
- [Hyperledger ACA-PyとOpen Wallet Foundation Credoを用いてOID4VCI上でJWT-VCの発行を試す](https://tech.gmogshd.com/try-oid4vci-acapy-afj/)

- [そういえばHyperledger AriesもOID4VCに対応するようです](https://idmlab.eidentity.jp/2024/02/hyperledger-ariesoid4vc.html)
- [credo-tsで「Email Verification Service」からemail VCを取得してみる(その2)](https://qiita.com/yk839/items/d73d4119d8d598c464e6)