OID4VCでスマホにVCを発行してみる

[**2024/2/17 update**] 
credo-ts libは、openid-credential-offerに含まれる"credentials":["OpenBadgeCredential"]をもとにissuerのmetadataを検索する。なのでmetadataに含まれる値、すなわち"credentials":["OpenBadgeCredentialJwt"]に変更
## はじめに
  最近EUDI walletの話をよく聞くようになってきた。EUDIとは欧州デジタルIDウォレットのことらしい([参考](https://datasign.jp/blog/european-digital-id-wallet-followup/))。IDウォレットでは[OpenID4VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html)/OpenID4VP+SIOPv2を使って、VCの受け渡しをするようである。

　図にすると以下のような感じなる(引用元：[globalPlatform](https://globalplatform.org/wp-content/uploads/2023/05/Overview-and-outlook-for-the-European-Digital-Identity-%E2%80%93-Asa-Barton-EU-Commission.pdf))
 ![eudi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/e3d211d2-477c-270d-0ec9-3aefa2fdc5ba.png)

　このOpenID4VCIを試す事ができるものがないか探してみると「Sphereon Wallet」を見つけることが出来た。さらに都合のいいことにios app storeで配付されていた(https://github.com/Sphereon-Opensource/ssi-mobile-wallet)。

https://apps.apple.com/us/app/sphereon-wallet/id1661096796

　またOpenID4VCI/OpenID4VPをしゃべる**demoサーバ**も用意されていて、以下のサイトで試す事が出来る。

 https://ssi.sphereon.com/demo/issuer/
 
 このプロトコルの中身を見ながら、最終的には簡易的なopenID4VCIをしゃべるサーバをnodejsで作ってみようと思う。

## OpenID4VCIの動き
### 1) QRコードを発行
　[demoサーバ](https://ssi.sphereon.com/demo/issuer/)にブラウザでアクセスし、[manually fill out details]を選択して、適当なfirstname/lastname, emailを入力してcontinueをクリック。その後select credentials画面が出るので「University degree」を選択すると、下記のようなQRコードが発行される
 ![qr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/146e2c90-39ba-ffaa-25fb-7ead6b8180d6.png)

 QRコードの中身は以下のようになっている。
 ```
 openid-credential-offer://?credential_offer={
   "grants":{
     "urn:ietf:params:oauth:grant-type:pre-authorized_code":{
       "pre-authorized_code":"AZcbrjCMa9StPQxVQRQAf",
       "user_pin_required":false
     }
   },
   "credentials":["OpenBadgeCredential"],
   "credential_issuer":"https://ssi.sphereon.com/pf3"
}
 ```
### 2) Credential Issuer Metadata の用意
　QRコードを読み取ったスマホは、issuerのMetadata情報(issuerはどのURLでCredentialsを発行していて、どこでtokenをもらえるかなどが書かれたJSON)を取得しにくる。demoサーバの場合、```https://ssi.sphereon.com/pf3/.well-known/openid-credential-issue```がそのURLである。以下のようなjsonが用意されている。
 
 ``` metadata.json
{
    "credential_issuer":"https://ssi.sphereon.com/pf3",
    "credential_endpoint": "https://ssi.sphereon.com/pf3/credentials",
    "token_endpoint": "https://ssi.sphereon.com/pf3/token",
    "display":[{name":"Sphereon","description":"Sphereon JFF Plugfest3 Issuer"}],
    "credentials_supported":[
      {..}
      {..}
      {
        "display":[
          {
            "name":"Example University Degree",
            "description":"JFF Plugfest 3 OpenBadge (JWT)",
            "text_color":"#FFFFFF",
            "background_color":"#1763c1",
            "logo":{
              "url":"https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
              "alt_text":"Red, magenta and yellow vertical lines with 3 black dots and the text JFF, depicting the Jobs For the Future logo."
            }
          },
          {
            "locale":"en-US",
            "name":"Example University Degree",
            "description":"JFF Plugfest 3 OpenBadge (JWT)"",
            "text_color":"#FFFFFF",
            "background_color":"#1763c1",
            "logo":{
              "url":"https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
              "alt_text":"Red, magenta and yellow vertical lines with 3 black dots and the text JFF, depicting the Jobs For the Future logo."
            }
          }
        ],
        "id":"OpenBadgeCredentialJwt",
        "types":["VerifiableCredential","OpenBadgeCredential"],
        "format":"jwt_vc_json",
        "cryptographic_binding_methods_supported":["did:key"],
        "cryptographic_suites_supported":["EdDSA"]
      }
    ],
    "credential_supplier_config":{
      "templates_base_dir":"templates/sphereon",
      "template_mappings":[
        {..},
        {..},
        {
          "credential_types":["OpenBadgeCredential"],
          "template_path":"OpenBadgeCredential.hbs",
          "format":"jwt_vc_json"
        }
      ]
    }
  };
 ```
### 3) tokenを発行
　QRコードに書かれた```pre-authorized_code```を用いて、スマホはdemoサーバからtokenを取得する。curlで書くと以下になる
 ```
 curl https://ssi.sphereon.com/pf3/token -X POST -H "Content-Type: application/json" -d @data1.txt
 ```
 ```data1.txt 
{
  "client_id": "sphereon:ssi-wallet",
  "grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
  "pre-authorized_code": "AZcbrjCMa9StPQxVQRQAf"
}
 ```
 POSTに成功すると、以下のような値が返ってくる
```
{
 "access_token":"eyJ0eXAiOiJKV1....6Vg8v4xrWbxA",
 "token_type":"bearer",
 "expires_in":300,
 "c_nonce":"48aa8701-dbec-4e56-a49e-2d9bf83f7ec3",
 "c_nonce_expires_in":300000,
 "authorization_pending":false,
 "interval":300000
 }
```
access_tokeの中身は、以下のようなJWTになっている。
```
{"typ":"JWT", "alg":"ES256K"}.
{
  "iat":1704349765484, "exp":300, 
  "iss":"https://ssi.sphereon.com/pf3",
  "preAuthorizedCode":"AZcbrjCMa9StPQxVQRQAf"
}.signature
```
### 4) VCを発行
　スマホは、metadata情報にｋ```https://ssi.sphereon.com/pf3/credentials```に、3)で取得したaccess_tokenを用いてアクセスすることでVCを取得することができる。curlで書くと以下になる。
 ```
 curl https://ssi.sphereon.com/pf3/credentials -X POST -H "Content-Type: application/json" -H "authorization: Bearer eyJ0eXAiOiJKV1....6Vg8v4xrWbxA" -d @data2.txt
 ```
```data2.txt 
{
  "types": [ "OpenBadgeCredential" ],
  "format": "jwt_vc_json",
  "proof": {
    "proof_type": "jwt",
    "jwt": "eyJhbGciOiJFUzI1Nksi....7GootXw"
  }
}
 ```
このdata2.txtに書かれているjwtをbase64デコードすると中身は以下の通りである。token取得時にdemoサーバから発行されたc_nonceや、issuer endpoint:```https://ssi.sphereon.com/pf3```を用いて生成されている。
```
{
  "alg":"ES256K",
  "typ":"openid4vci-proof+jwt",
  "kid":"did:key:zQ3shdST2uDCn8eNRhTNYs3Yh6dDx9aYQSV7gYyFfhc9kCxnm"}.
{
  "iat":1704349849,"exp":1704353449,
  "aud":"https://ssi.sphereon.com/pf3",
  "nonce":"48aa8701-dbec-4e56-a49e-2d9bf83f7ec3",
  "iss":"sphereon:ssi-wallet",
  "jti":"C289A6F6-7731-49B6-BC4A-CF8247369D2D"
}.signature
```
POSTが成功すると、VC credentialが入ったJSONが返ってくる。
```
{
  "credential":"eyJhbGciOiJFUzI1NiIsInR5c....kX8s64CusI6Tz-ZtJkQ",
  "format":"jwt_vc_json",
  "c_nonce":"1fbd4fa8-cd62-4b90-a07f-9201d97f9951",
  "c_nonce_expires_in":300000
}
```
credentialの中身をデコードすると、以下のようになっている。
```
{"alg":"ES256","typ":"JWT"}.
{
	"exp":1704954740,
	"vc":{
		"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json"],
		"type":["VerifiableCredential","OpenBadgeCredential"],
		"credentialSubject":{
			"type":["AchievementSubject"],
			"achievement":{
				"id":"urn:uuid:ac254bd5-8fad-4bb1-9d29-efd938536926",
				"type":["Achievement"],
				"name":"JFF x vc-edu PlugFest 3 Interoperability",
				"description":".........<snip>.......",
				"criteria":{
					"type":"Criteria",
					"narrative":".........<snip>......."
				},
				"image":{
					"id":"https://w3c-ccg.github.io/vc-ed/plugfest-3-2023/images/JFF-VC-EDU-PLUGFEST3-badge-image.png",
					"type":"Image"
				}
			},
			"id":"did:key:zQ3shdST2uDCn8eNRhTNYs3Yh6dDx9aYQSV7gYyFfhc9kCxnm"
		}
	},
	"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json"],
	"type":["VerifiableCredential","OpenBadgeCredential"],
	"expirationDate":"2024-01-11T06:32:20.016Z",
	"name":"JFF x vc-edu PlugFest 3 Interoperability",
	"issuer":{
		"type":["Profile"],
		"name":"Jobs for the Future (JFF)",
		"url":"https://www.jff.org/",
		"image":"https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
		"id":"did:jwk:eyJhbGciOiJ....M4In0"
	},
	"credentialSubject":{
		"type":["AchievementSubject"],
		"achievement":{
			"id":"urn:uuid:ac254bd5-8fad-4bb1-9d29-efd938536926",
			"type":["Achievement"],
			"name":"JFF x vc-edu PlugFest 3 Interoperability",
			"description":".........<snip>.......",
			"criteria":{
				"type":"Criteria",
				"narrative":".........<snip>......."
			},
			"image":{
				"id":"https://w3c-ccg.github.io/vc-ed/plugfest-3-2023/images/JFF-VC-EDU-PLUGFEST3-badge-image.png",
				"type":"Image"
			}
		},
		"id":"did:key:zQ3shdST2uDCn8eNRhTNYs3Yh6dDx9aYQSV7gYyFfhc9kCxnm"
	},
	"issuanceDate":"2024-01-04T06:32:20.016Z",
	"sub":"did:key:zQ3shdST2uDCn8eNRhTNYs3Yh6dDx9aYQSV7gYyFfhc9kCxnm",
	"nbf":1704349940,
	"iss":"did:jwk:eyJhbGciOiJ....M4In0"
}.signature

```

## 簡易サーバで動作確認
　demoサーバ：```https://ssi.sphereon.com/demo/issuer/```の動きをnodejsで実装し、SSI sphereon walletにVCを発行してみる
#### 1. index.html, server.js, package.jsを以下のフォルダ構成のように配置する
 ```
├── server.js
├── package.json
└── views/
    ├── index.html
```

<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">[ngrok openBadge issue]</div><br>
    <div id="parameter">University:<input type="text" id="university" value="xx University"><br>
    achievement:<input type="text" id="achievement" value="Math."><br>
    <input type="button" value="発行" onclick="issuerQR()"/><br>
    <p><div id=qrmsg></div>
    <p><div id=qr></div>
  </div>

</body>
<script type="text/javascript">

  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);
  }

  function issuerQR() {
    let setting={
      "clientName": "ngrok openBadge issue",
      "type": "openBadge",
      "claims": {"university": document.getElementById('university').value, "achievement": document.getElementById('achievement').value}
    }
    sendData("./.issuer_init", JSON.stringify(setting), 'application/json', 
      function(data){
        target = document.getElementById("qr");
        target.innerHTML = "<img src="+JSON.parse(data).qrcode+" />";
        let interval_id = setInterval(
          function(){
            console.log("request check status");
            istatusCheck(interval_id, JSON.parse(data).sess);
          }, 3000);
      }
    );
  }

  function istatusCheck(clearid, sess){
    getData("./.istatus/"+sess, 'application/json', 
      function(data){
        console.log("status:"+JSON.parse(data).status);
        if (JSON.parse(data).status == "request_retrieved"){
          let msg = "QR code sccand. waiting..";
          if (typeof JSON.parse(data).pin !=="undefined"){ msg +="PIN:"+JSON.parse(data).pin;}
          document.getElementById("qrmsg").innerHTML =msg;
        }else if(JSON.parse(data).status == "issuance_successful"){
          document.getElementById("qrmsg").innerHTML = "finish";
          document.getElementById("qr").innerHTML = "";
          clearInterval(clearid);
        }
      }
    );
  }

  </script>
</html>
```
</details>
<details>
<summary>server.jsのコードを見る</summary>

``` server.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const http = require('http');
const QRCode = require('qrcode');
const crypto = require('crypto');
const elliptic = require('elliptic');
const didJWT = require('did-jwt');
const fs = require('fs');
const uuid = require('uuid');
const base58 = require('bs58');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use('/', express.static(__dirname + '/views/'));

//*******setting********
const port=8888;
let hostname = '127.0.0.1'+":"+port;
if (process.argv.length > 2){ hostname = process.argv[2];}
//**********************

//*******global variable*******
let DID="did:web:"+hostname;
let task= new Array();
let taskstatus= new Array();
let storageJson=[];
let didwebkey, didkey, didjwk;

const main = async()=>{
  didwebkey = await init(hostname);
  didkey = a_did(didwebkey, "key");
  didjwk = a_did(didwebkey, "jwk");
  console.log("\n " + DID)
  console.log(" " + didkey)
  console.log(" " + didjwk+"\n")

}
main();
//******************************

//*******web server*******
const httpServer = http.createServer(app);
const server = httpServer.listen(port, () => {
  console.log('\n web page start: listening on port %s...', server.address().port);
});

app.get('/.*', async function(req,res){
	console.log("\n====GET====");
	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("====GET====");

  let ret="";
  let cmd = req.params[0].split('/');

  if (cmd[0] =="pf" && cmd[1] ==".well-known" && cmd[2] == "openid-credential-issuer"){
    console.log("access to openid-credential-issuer")
    let _metadata = await metadata(hostname, "");
    res.send(_metadata);

  } else if (cmd[0] =="istatus"){
    ret={"status":taskstatus[cmd[1]].vcstatus};
    res.send(JSON.stringify(ret));

  }else{
    res.send(ret);
  }
});

app.post('/.*', async function(req,res){
	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"){
    let preAuthorizedCode = await issuerInit(req.body);
    let offer = {
      "grants":{
        "urn:ietf:params:oauth:grant-type:pre-authorized_code":{
          "pre-authorized_code":preAuthorizedCode,
          "user_pin_required":false
        }
      },
      "credentials":["OpenBadgeCredentialJwt"],
      "credential_issuer":"https://"+hostname+"/.pf"
    }
    let inviteUrl = "openid-credential-offer://?credential_offer="+encodeURIComponent(JSON.stringify(offer));
    QRCode.toDataURL(inviteUrl, function (err, url) {
      let sendData = {"qrcode":url, "sess":task[preAuthorizedCode].sess, "url":task[preAuthorizedCode].serviceEndpoint};
      res.send(sendData);
    });
  }else if(cmd[0] == "pf" && cmd[1]=="token"){
     let token={};
     let authcode = req.body['pre-authorized_code'];
     if (typeof task[authcode] !== "undefined"){
       console.log("authenticated!!");
       task[authcode].client_id = req.body.client_id;
       task[authcode].nonce = uuid.v4();
       let atoken = await makeToken(authcode, 300);
       token = {
         "access_token":atoken,
         "token_type":"bearer",
         "expires_in":300,
         "c_nonce":task[authcode].nonce,
         "c_nonce_expires_in":300000,
         "authorization_pending":false,
         "interval":300000
       }
       res.send(JSON.stringify(token))
     }else{
       console.log("unauthenticated!!:"+ req.body['pre-authorized_code']);
       res.send(JSON.stringify(token))
     }
  }else if (cmd[0] == "pf" && cmd[1]=="credentials"){
    let credJson={}
    let auth = req.headers.authorization;
    let token = didJWT.decodeJWT(auth.split(" ")[1]);
    if (typeof task[token.payload.preAuthorizedCode] !== "undefined"){
      let _jwt = didJWT.decodeJWT(req.body.proof.jwt);
      if (task[token.payload.preAuthorizedCode].nonce == _jwt.payload.nonce 
          && task[token.payload.preAuthorizedCode].serviceEndpoint == _jwt.payload.aud){
        let _kid = _jwt.header.kid;
        let cred = await makecred(
            _kid, uuid.v4(), 
            Math.floor( new Date().getTime() / 1000 ) , 30*24*60*60,  // from now to 1 month
            task[token.payload.preAuthorizedCode].sess
        );
        let credJson = {
          "credential":cred,
          "format":"jwt_vc_json",
          "c_nonce":uuid.v4(),
          "c_nonce_expires_in":300000
        }
        taskstatus[task[token.payload.preAuthorizedCode].sess].vcstatus="issuance_successful";
        res.send(JSON.stringify(credJson))
      }else{
        console.log("bad nonce or bad aud")
        res.send(JSON.stringify(credJson))
      }
    }else{
      console.log("unauthentication: bad token")
      res.send(JSON.stringify(credJson))
    }
  }else{
    res.send(null);
  }
});


//////////////////////
//  support function
///////////////////////

async function issuerInit(_body){
  let preAuthorizedCode = base58.encode(crypto.randomBytes(18));
  console.log(preAuthorizedCode)
  let sess = crypto.randomBytes(8).toString("hex")
  let serviceEndpoint = "https://"+hostname+"/.pf";

  task[preAuthorizedCode]= {
    "claims": _body.claims,
    "preAuthorizedCode":preAuthorizedCode,
    "serviceEndpoint":serviceEndpoint,
    "isskey": didwebkey,
    "sess": sess
  };
  taskstatus[sess]={"vcstatus":"wait", "claims":_body.claims};
  return preAuthorizedCode;
}


async function metadata(_hostname, _sess){
  let _endpoint = "https://"+_hostname+"/.pf";
  let ret = {
    "credential_issuer":_endpoint,
    "credential_endpoint": _endpoint + "/credentials",
    "token_endpoint": _endpoint + "/token",
    "display":[{"name":"ngrok:"+hostname,"description":"ngrok Issuer:"+hostname}],
    "credentials_supported":[{
      "display":[
        {
          "name":"xx university", 
          "description":"OpenBadge (JWT)",
          "text_color":"#FFFFFF",
          "background_color":"#1763c1",
          "logo":{
            "url":"https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
            "alt_text":"Red, magenta and yellow vertical lines with 3 black dots and the text JFF, depicting the Jobs For the Future logo."
          }
        },
        {
          "locale":"en-US",
          "name":"xx university", 
          "description":"OpenBadge (JWT)",
          "text_color":"#FFFFFF",
          "background_color":"#1763c1",
          "logo":{
            "url":"https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
            "alt_text":"Red, magenta and yellow vertical lines with 3 black dots and the text JFF, depicting the Jobs For the Future logo."
          }
        }
      ],
      "id":"OpenBadgeCredentialJwt",
      "types":["VerifiableCredential","OpenBadgeCredential"],
      "format":"jwt_vc_json",
      "cryptographic_binding_methods_supported":["did:key"],
      "cryptographic_suites_supported":["EdDSA"]
    }],
    "credential_supplier_config":{
      "templates_base_dir":"templates/sphereon",
      "template_mappings":[
        {
          "credential_types":["OpenBadgeCredential"],
          "template_path":"OpenBadgeCredential.hbs",
          "format":"jwt_vc_json"
        }
      ]
    }
  };
  return ret;
}

async function makecred(_subdidkey, _uuid, _nbf, _exp, _sess){
  const signer = didJWT.ES256KSigner(didJWT.hexToBytes(Buffer.from(didwebkey.privateJwk.d, 'base64').toString('hex')))
  let _body = {
    "vc":await makevc(_subdidkey, _uuid, taskstatus[_sess].claims.achievement),
    "@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json"],
    "type":["VerifiableCredential","OpenBadgeCredential"],
    "name": taskstatus[_sess].claims.university +"'s openBadge",
    "issuer":{
      "type":["Profile"],
      "name":"Jobs",
      "url":"https://www.jff.org/",
      "image":"https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png",
      "id":didjwk
    },
    "credentialSubject":{
      "type":["AchievementSubject"],
      "achievement":{
        "id":"urn:uuid:"+_uuid,
        "type":["Achievement"],
        "name":taskstatus[_sess].claims.university +"'s openBadge",
        "description":"demo",
        "criteria":{
          "type":"Criteria",
          "narrative":"..."
        },
        "image":{
          "id":"https://w3c-ccg.github.io/vc-ed/plugfest-3-2023/images/JFF-VC-EDU-PLUGFEST3-badge-image.png",
          "type":"Image"
        }
      },
      "id": _subdidkey
    },
    "sub": _subdidkey,
    "iss": didjwk,
    "nbf": _nbf
  }
  let jwt = await didJWT.createJWT(
    _body,
    { issuer: didjwk, expiresIn:_exp, signer },
    { alg: 'ES256K' , typ:'JWT'}
  )
  return jwt
}

async function makevc(_subdidkey, _uuid, name){
  let _vc = {
    "@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json"],
    "type":["VerifiableCredential","OpenBadgeCredential"],
    "credentialSubject":{
      "type":["AchievementSubject"],
      "achievement":{
        "id":"urn:uuid:"+_uuid,
        "type":["Achievement"],
        "name": name,
        "image":{
          "id":"https://w3c-ccg.github.io/vc-ed/plugfest-3-2023/images/JFF-VC-EDU-PLUGFEST3-badge-image.png",
          "type":"Image"
        }
      },
      "id":_subdidkey
    }
  };
  return _vc;

}


async function init(_hostname){
  let _did = "did:web:"+_hostname;
  let _key = await loadDid(_did);
  if (_key ==null){
    console.log("\n this "+_did+"'s key not registered")
    let _key = await createkey();
    await saveDid(_did, _key);
    return _key;
  }else{
    console.log("\n this "+_did+"'s key is registered")
    return _key;
  }
}

async function loadDid(_did){
  let key = null;
  try{
    storageJson = JSON.parse(fs.readFileSync('./storage.json'));
    storageJson.forEach((item) =>{
       if (item.did == _did) key = item.key
    })
    return key;
  }catch(err){
    return null;
  }
}

async function saveDid(_did, _key){
  storageJson.push({did:_did, key:_key})
  try{
    fs.writeFileSync("./storage.json", JSON.stringify(storageJson))
  }catch(err){
    console.error(err);
  }
}

async function createkey(){
  const key = crypto.randomBytes(32).toString("hex");
  const ec = new elliptic.ec('secp256k1');
  const prv = ec.keyFromPrivate(key, 'hex');
  const pub = prv.getPublic();
  const ecjwk = {
    kty:"EC", 
    crv:"secp256k1", 
    "x":pub.x.toBuffer().toString('base64'),
    "y":pub.y.toBuffer().toString('base64')
  };
  const ecjwkpri = {
    kty:"EC", 
    crv:"secp256k1", 
    "x":pub.x.toBuffer().toString('base64'),
    "y":pub.y.toBuffer().toString('base64'),
    "d":prv.getPrivate().toBuffer().toString('base64')
  };
  let _key={"publicJwk":ecjwk,"privateJwk":ecjwkpri};
  return _key;
}

function a_did(key, method){
  let ret = "";
  if (method == "key"){
    if (key.publicJwk.kty == "EC" && key.publicJwk.crv=="secp256k1"){
      let yhex = Buffer.from(key.publicJwk.y, 'base64').toString('hex')
      if ((parseInt(yhex.slice( -1 ), 16) %2)==1){
        parity_flag = "03";
      }else{
        parity_flag = "02";
      }
      let tmp = "E701" + parity_flag + Buffer.from(key.publicJwk.x, 'base64').toString('hex');
      ret = "did:key:z"+base58.encode(Buffer.from(tmp, 'hex'));
    }
  }else if(method == "jwk"){
    if (key.publicJwk.kty == "EC" && key.publicJwk.crv=="secp256k1"){
      let jwt = {"alg":"ES256K","use":"sig","kty":"EC","crv":"secp256k1","x":key.publicJwk.x.replaceAll("=",""),"y":key.publicJwk.y.replace("=", "")}
      ret = "did:jwk:"+Buffer.from(JSON.stringify(jwt)).toString('base64').replaceAll("=", ""); 
    }
  }else{
  }
  return ret;
}

async function makeToken(authcode, exp){
  const signer = didJWT.ES256KSigner(didJWT.hexToBytes(Buffer.from(task[authcode].isskey.privateJwk.d, 'base64').toString('hex')))
  let jwt = await didJWT.createJWT(
    { preAuthorizedCode: authcode},
    { issuer: task[authcode].serviceEndpoint, expiresIn:exp, signer },
    { alg: 'ES256K' , typ:'JWT'}
  )
  return jwt
}
```
</details>
<details>
<summary>package.jsonのコードを見る</summary>

``` package.json
{
  "dependencies": {
    "body-parser": "^1.20.2",
    "bs58": "^5.0.0",
    "did-jwt": "^7.4.7",
    "elliptic": "^6.5.4",
    "express": "^4.18.2",
    "qrcode": "^1.5.3",
    "uuid": "^9.0.1"
  }
}
```
</details>

#### 2. 関連モジュールをインストールする
```
> npm install
```
#### 3. サーバに割り当てるドメイン名を取得
```
> ngrok http 8888
Region                   United States (us)
Web Interface            http://127.0.0.1:4040
Forwarding               http://57dd-180-53-77-xxx.ngrok-free.app -> http://localhost:8888
Forwarding               https://57dd-180-53-77-xxx.ngrok-free.app -> http://localhost:8888
```
#### 4. 取得したドメイン名を引数に、サーバを起動する
```
> nodejs server.js 57dd-180-53-77-xxx.ngrok-free.app

 web page start: listening on port 8888...

 this did:web:6f3d-153-219-137-227.ngrok-free.app's key is registered

 did:web:57dd-180-53-77-xxx.ngrok-free.app
 did:key:zQ3shq4taAWbUYmvaf4gNmfR9ZtJ1wN8nauwRxnacLQarYTCp
 did:jwk:eyJhbGciOiJFUzI1NksiLCJ1c2UiOiJzaWciLCJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJtdTR4b0JHV0lmd0VvYkoyN3krcitMYjhCSklqaC9kU1pYRmlRaEN6clIwIiwieSI6InJ5Nkh5QmVqNENOVllveGZZYmQrOW1ibVZWU0VySEdDUUQ1NnJ5TFFrenMifQ
```
#### 5. ブラウザでhttps://57dd-180-53-77-xxx.ngrok-free.app/にアクセス
1. 画面上にある「発行」ボタンを押してQRコードを表示<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/a98b4fec-5142-4926-6545-8c446620fd11.png" width=60%>

1. 表示されたQRコードを、スマホでスキャンするとVC発行の手続きが始まる
![sma.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/73f69796-c054-0311-cf81-23b362fd463d.png)