EntraのfaceCheckを体験してみる

[**2024/02/10**] Woodgrove Verified ID demo siteサイトで顔写真入りのVC発行がerrorになっているので、体験サンプルにVC発行機能を追加

## はじめに
　下記記事にあるように、スマホに入っているAthenticator/id walletを触って遊んでいる。中でもMS authenticatorは、独自機能が試験的に盛り込まれていて触っていて面白い。

https://qiita.com/yk839/items/0f346f0aa4b6d029ecd6

　そのMS authenticatorにfaceCheck機能が入っているという話を聞いたので、さっそく中身を見ていきたいと思う。

https://idmlab.eidentity.jp/2024/02/entra-verified-idmicrosoft-authenticator.html

　faceCheck機能はVerifiable Credential(vc)の中に顔写真を埋め込み、verifierサイトにログインするときに、そのVCの持ち主かどうかをVCに埋め込まれた顔写真で確認するものである。
 ![face.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/bd8d69a7-9e1c-94b5-b3a7-ee726ce4885a.png)

　チケットの転売などがVCになってきてスマホに紐付けられるようになっても、転売ヤーはスマホごと売り払えばいいという話になってしまうが、VC型のチケット発行時に購入者の写真をVCに埋め込んで発行すれば、そういう心配はいらないという感じなんだろう。そういうタイプのユースケースには響きそうな機能なんだなと感じる。

 ## faceCheckの流れ
 ### 1. スマホにVCを発行してもらう
 [Woodgrove Verified ID demo site](https://woodgrovevid.com/IdTokenHint/issue)にアクセスして、「Student Card」を発行してもらう。発行画面は下記のような感じである。Optionであるが、「choose File」で手持ちの顔写真(*.webp)を選択すると下記のような登録画面になる。
 ![reg1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/fd033062-8d12-e821-b67c-898b3cf827e5.png)

 このときスマホに発行されたVCを見てみると下記のようになっている。claimの中にphotoという項目が増え、そこにはwebpフォーマットの接頭語が見えるので、登録時にuploadした顔写真のraw dataをそのまま放り込んでいる感じである。
 ```json
 {
  "vc": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1"
    ],
    "type": [
      "VerifiableCredential",
      "StudentCard"
    ],
    "credentialSubject": {
      "id": "4651575414",
      "firstName": "test",
      "lastName": "Example",
      "photo": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAA.<snip>.Hemwsf2cbsqSd33/lb2z5UUTb2f/Z"
    },
    "credentialStatus": {
      "id": "urn:uuid:1b23a846-040b-4508-8b75-cdb5bf4659a1?bit-index=116",
      "type": "RevocationList2021Status",
      "statusListIndex": 116,
      "statusListCredential": "did:web:verifiedid.entra.microsoft.com:1f9ef086-c2be-462a-9f4e-88f0a4d01691:03640502-457c-f278-996a-ffd9cb83c308?service=IdentityHub&queries=W3sibWV0aG9kIjoiQ29sbGVjdGlvbnNRdWVyeSIsInNjaGVtYSI6Imh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSIsIm9iamVjdElkIjoiMWIyM2E4NDYtMDQwYi00NTA4LThiNzUtY2RiNWJmNDY1OWExIn1d"
    }
  },
  "jti": "urn:pic:b4c8377a4be44673b5ee17458c1e25cd",
  "iss": "did:web:verifiedid.entra.microsoft.com:1f9ef086-c2be-462a-9f4e-88f0a4d01691:03640502-457c-f278-996a-ffd9cb83c308",
  "sub": "did:ion:EiAn6Yue05YAACxU8mHts_83cm9AHuYDVO1ljEegAYAwgw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWduX0c4QnpsbjlNSlAiLCJwdWJsaWNLZXlKd2siOnsiYWxnIjoiRVMyNTZLIiwiY3J2Ijoic2VjcDI1NmsxIiwia2V5X29wcyI6WyJ2ZXJpZnkiXSwia2lkIjoic2lnbl9HOEJ6bG45TUpQIiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiTzh4T2oxY09VVGpadzlUTkh6NzE4OGFfOWNQWTMtQkxJZElXa0xGY0R1MCIsInkiOiI3Wko0U2FBNVJXN3ZNM2VrNTNtMGN1NEtxUmUyOUtrQzJDaWNxSkpnTy1ZIn0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkifV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpQU55VzV3SElmRjRBb0RmVUNoQ3Y0MkM5NFhKTkNxX0F4bWlkWDZYc2VjLVEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUMxYU1wYmRQQmVoelBtVC1hRlpmZ2FNelZsOF9BZnhEczFqMlh1V2diUmJBIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlBT2g0ZXRKUWZLalljV3RsWlBuT1A4aUg1cmhUSjNUeUhJbkEyNGVfUTA4USJ9fQ",
  "iat": 1707020794,
  "exp": 1712275199
}
 ```
 デモサイトから発行されたVCをMS Authenticatorアプリで見てみると、顔写真が入っているのがわかる。この辺りはmanifestでclaim photoの内容をdisplayするように記載してあるからなんだろうな..。
![IMG_3254-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/67b94180-1a62-6494-9a01-7e95b12d657d.png)

### 2. verifierでvcを検証する(+faceCheck)
#### 2.1　invite用QRコードを提示
　以下のようなQRコードをMS authenticatorでスキャンする。読み取れる内容は、URL(```openid-vc://?request_uri=https://5070-153-132-214-182.ngrok-free.app/.presentationRequests/ebab5252be665477```)である。

![qr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/c26fcd1f-527c-5bf7-2545-f7944b9ec3e8.png)
### 2.2 faceCheck
 スキャンすると、MS authenticatorアプリは、redirect_uriに示された```https://5070-153-132-214-182.ngrok-free.app/.presentationRequests/ebab5252be665477```からrequestを読み出し、そのrequestに書かれた内容に従って、以下のような画面遷移を起こす。
![face1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/96324994-c233-06f4-3083-603a33d0a5db.png)

MS Authenticatorが読み取ったrequestは、以下のような内容である。このrequestの中でfaceCheck特有の値はinput_descriptorsに書かれている```{"uri": "LivenessCheck"}```である。これが指定されることでMS authenticatorは顔の読み取りを行うようだ。
```json
{
  "alg": "ES256K",
  "kid": "did:web:5070-153-132-214-182.ngrok-free.app#owner",
  "typ": "JWT"
}.{
  "iat": 1707042076,
  "exp": 1707045676,
  "jti": "c430775d-d6f1-4f27-ba07-b565fa891921",
  "response_type": "id_token",
  "response_mode": "post",
  "scope": "openid",
  "nonce": "eq+eC75SXMtcarpPAP60+Q==",
  "client_id": "did:web:5070-153-132-214-182.ngrok-free.app",
  "redirect_uri": "https://5070-153-132-214-182.ngrok-free.app/.presentationRequests/64819363fd253117",
  "state": "123456789",
  "registration": {
    "client_name": "839 home",
    "subject_syntax_types_supported": ["did:ion","did:key","did:jwk"],
    "vp_formats": {
      "jwt_vp": { "alg": ["ES256K","EdDSA"] },
      "jwt_vc": { "alg": ["ES256K","EdDSA"] }
    }
  },
  "claims": {
    "vp_token": {
      "presentation_definition": {
        "id": "b3a5f8a8-c7f7-427b-ade0-3611f84f6314",
        "input_descriptors": [
          {
            "id": "Any student card",
            "purpose": "So we can see accept any studentCard scheme",
            "schema": [{"uri": "StudentCard"}]
          },
          {
            "id": "a2374f30-fa89-40fe-9e0b-47dc3fd736a0",
            "name": "LivenessCheck",
            "schema": [{"uri": "LivenessCheck"}]
          }
        ]
      }
    }
  },
  "iss": "did:web:5070-153-132-214-182.ngrok-free.app#owner"
}.[signature]
```
### 2.3 verifierにMS Authenticatorからpostされる内容
　スマホは顔の読み取りを終えると、verifierに結果を返してくれる。返し先はrequestに書かれた```"redirect_uri": "https://5070-153-132-214-182.ngrok-free.app/.presentationRequests/64819363fd253117```に```"response_mode": "post"```で書き込みが行われる。書き込まれるjsonは下記内容である。
```json
{ 
  id_token:"eyJ0eXAiOiJ....UzI1NksiLCJraWQiOiJkaWQ6aW9uOkVpQW42WXVlMDVZQUFDeFU4bUh0c....ZDRmYWIxLTU0ODktNGRlZi1hMDI0LTkwY2VkODM3MDZkZCIsImlkIjoiM0NDQjg1NDUtQkNGOS00QzFFLThGRTAtQjg0REIzNjE5MDU4In19fQ.1oACywpUGgkpLKQNGXCRLsiPVFlirBfs5MnihQSa401WpB24buqXCOgNz6NlSBa26fXjOZVibJRV_OL6RiWb6Q",
  vp_token: "eyJraWQiOiJ.....BvMFNXcHZhVlI2YURSVU1tOTRXVEE1VmxaSGNHRmtlbXhWVkd0b05rNTZSVFJQUjBabVQxZE9VVmRVVFhSUmEzaEtXa1ZzV0dFd2VFZFpNRkl4VFVOSmMwbHVhMmxQYVVrelYydHZNRlV5UmtKT1Z",
  state: "123456789"
}
```
この中のvp_tokenの中身を詳しく見ていくと、以下のように2つのVC1, VC2が入っていることがわかる。VC1は上記で紹介したClaimにphotoが入ったstudentCard VCである。

```json
{
    "kid":"did:ion:EiAn6Yue05YAACxU8mHts_83cm9AHuYDVO1ljEegAYAwgw:eyJkZWx0YSI...VfUTA4USJ9fQ#sign_G8Bzln9MJP",
    "typ":"JWT",
    "alg":"ES256K"
},{
  "jti":"C6A5DDA5-CC7F-4269-9123-57D70742479A",
  "iat":1707043255,
  "iss":"did:ion:EiAn6Yue05YAACxU8mHts_83cm9AHuYDVO1ljEegAYAwgw:eyJkZWx0YSI...VfUTA4USJ9fQ",
  "nbf":1707043255,
  "aud":"did:web:5070-153-132-214-182.ngrok-free.app",
  "exp":1707046255,
  "nonce":"MN1mt9y9Oh672kB1sYN5qw==",
  "vp":{
    "verifiableCredential":[
        "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2....", // VC1
        "eyJraWQiOiJkaWQ6aW9uOkVpQW42WXVlMD..."       // VC2
    ],
    "@context":["https://www.w3.org/2018/credentials/v1"],
    "type":["VerifiablePresentation"]
  }
}.[signature]
```
VC2は、self-signのVCでスマホ上で撮影された顔写真の動画が入っている。ここはスマホで照合してその結果だけがverifier側に渡るものだと思っていたので、意外な動きであった、恐らくpreviewer版だからかなと考えられる。というのも、MS authenticator内では、顔画像は共有されませんって書かれたUIが表示されているので、正式版ではその対応が図られるものと思われる。

[**2024/02/12**] [https://learn.microsoft.com/ja-jp/entra/verified-id/using-facecheck](https://learn.microsoft.com/ja-jp/entra/verified-id/using-facecheck)
>顔チェックの生体認証ビジョン チェックはモバイル デバイスで実行されるのですか?
いいえ。 写真とキャプチャされたライブネス データの間の生体認証チェックは、Azure AI Vision Face API を使用してクラウドで実行されます。 プロセス中にユーザーがキャプチャした自撮りは、要求元の ID 検証サイトと共有されません。

--> verifierが「Request Service API 」を使うことを前提としたプライバシーモデルなのか...。

```
{
  "kid": "did:ion:EiAn6Yue05YAACxU8mHts_83cm9AHuYDVO1ljEegAYAwgw:eyJkZWx0YSI...VfUTA4USJ9fQ#sign_G8Bzln9MJP",
  "typ": "JWT",
  "alg": "ES256K"
}.{
  "sub": "did:ion:EiAn6Yue05YAACxU8mHts_83cm9AHuYDVO1ljEegAYAwgw:eyJkZWx0YSI...VfUTA4USJ9fQ",
  "iss": "did:ion:EiAn6Yue05YAACxU8mHts_83cm9AHuYDVO1ljEegAYAwgw:eyJkZWx0YSI...VfUTA4USJ9fQ",
  "iat": 1707043242467,
  "jti": "30062CA9-846D-46D4-BA24-D3ABF3AE2F10",
  "exp": 1707043542467,
  "vc": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1"
    ],
    "type": [
      "VerifiableCredential",
      "LivenessCheck"
    ],
    "credentialSubject": {
       "metadata": "{\"metadata\":{\"imageData\":[{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Bright\"},\"fileName\":\"video.webp\",\"frameNumber\":0,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241676,\"targetFaceRectangle\":{\"height\":639,\"left\":250,\"top\":744,\"width\":650},\"timeOffsetWithinFileInMilliseconds\":0},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Bright\"},\"fileName\":\"video.webp\",\"frameNumber\":1,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"description\":\"Nose\",\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241677,\"targetFaceRectangle\":{\"height\":218,\"left\":416,\"top\":939,\"width\":218},\"timeOffsetWithinFileInMilliseconds\":1},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Bright\"},\"fileName\":\"video.webp\",\"frameNumber\":2,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"description\":\"LeftEye\",\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241678,\"targetFaceRectangle\":{\"height\":94,\"left\":318,\"top\":883,\"width\":217},\"timeOffsetWithinFileInMilliseconds\":2},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Bright\"},\"fileName\":\"video.webp\",\"frameNumber\":3,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"description\":\"RightEye\",\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241679,\"targetFaceRectangle\":{\"height\":93,\"left\":548,\"top\":873,\"width\":217},\"timeOffsetWithinFileInMilliseconds\":3},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Dark\"},\"fileName\":\"video.webp\",\"frameNumber\":4,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241745,\"targetFaceRectangle\":{\"height\":642,\"left\":248,\"top\":742,\"width\":655},\"timeOffsetWithinFileInMilliseconds\":69},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Dark\"},\"fileName\":\"video.webp\",\"frameNumber\":5,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"description\":\"Nose\",\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241746,\"targetFaceRectangle\":{\"height\":219,\"left\":420,\"top\":934,\"width\":219},\"timeOffsetWithinFileInMilliseconds\":70},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Dark\"},\"fileName\":\"video.webp\",\"frameNumber\":6,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"description\":\"LeftEye\",\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241747,\"targetFaceRectangle\":{\"height\":94,\"left\":319,\"top\":880,\"width\":219},\"timeOffsetWithinFileInMilliseconds\":71},{\"activeIlluminationInformation\":{\"illuminatedColor\":\"Illumination_Dark\"},\"fileName\":\"video.webp\",\"frameNumber\":7,\"imageOffsetsIfCropped\":{\"cropOffsetX\":0,\"cropOffsetY\":0,\"fullFrameImageHeight\":1920,\"fullFrameImageWidth\":1080},\"imageProperties\":{\"description\":\"RightEye\",\"exposureTimeInSeconds\":null,\"fNumber\":null,\"isoSpeed\":-1},\"imageTimestampInMilliseconds\":1707043241748,\"targetFaceRectangle\":{\"height\":95,\"left\":552,\"top\":871,\"width\":220},\"timeOffsetWithinFileInMilliseconds\":72}],\"imageType\":0,\"version\":\"1.0\"}}"
       "content": "UklGRvzaAwBXR....jdVAgAAAA" //--スマホで撮影された顔写真の動画
    }
  }
}.[signature]
```
### 2.4 verifierで検証
previewe版だと、postされてきたVPの中からVC1, VC2を読み出し、VC1の中のphoto claimと、VC2の顔画像の動画を照合し、OK/NGを返す処理イメージである。

## 体験してみよう
#### 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 registration & verifier]</div>
<table border=1><tr><td>
    <div id="parameter">given name:<input type="text" id="given" value="hogehoge"><br>
    family name:<input type="text" id="family" value="deadbeaf"><br>
    photo:<button id="switchFrontBtn" onclick="switchCamera('user'); return false;"">Take a selfie</button>
    <button id="snapBtn" onclick="takePicture(); event.preventDefault() ; return false;" style="display: none;" class="btn btn-outline-warning">Snap</button><br>
    <video id="cam" autoplay="" muted="" playsinline="" style="width: 200px; height: 200px; display: none;">Not available</video><canvas id="canvas" style="display:none"></canvas>
    <img id="selfie" width="200" style="max-height: 200px; margin-top: 20px;" src=""><br>
<button id="clearBtn" style="display: none;" class="btn btn-link link-warning" onclick="clearPhoto();">Remove</button><b>
    <input type="button" value="発行" onclick="issuerQR()"/><br>
    <input type="hidden" id="imageUploadStr" class="form-control" data-val="true" data-val-required="The Photo field is required." name="Photo" value="">
    <p><div id=qrmsg></div>
    <p><div id=qr></div>
  </div>
</td><td>
  <input type="button" value="資格の検証" onclick="callQR()"/>
  <p><div id=qrmsg2></div>
  <p><div id=qr2></div>
</td></tr></table>
  <p><div id=result1 style="word-break: break-word;" align=left width=90%></div>
  <p><div id=result2 style="word-break: break-word;" align=left width=90%></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() {
    document.getElementById("qr2").innerHTML="";
    let photo = document.getElementById("selfie").src
    let setting={
      "clientName": "ngrok registration",
      "type": "ngrokVerifiedCredential",
      "claims": {"lastName": document.getElementById('family').value, "firstName": document.getElementById('given').value}
    }
    if (photo.length>2000){
      setting.photo = photo
    }
    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 callQR() {
    document.getElementById("qr").innerHTML="";
    getData("./.callQR", 'application/json', 
      function(data){
        target = document.getElementById("qr2");
        target.innerHTML = "<img src="+JSON.parse(data).qrcode+" />";
        let interval_id = setInterval(
          function(){
            console.log("request check status");
            vstatusCheck(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);
        }
      }
    );
  }
  function vstatusCheck(clearid, sess){
    getData("./.vstatus/"+sess, 'application/json', 
      function(data){
        console.log("status:"+JSON.parse(data).status);
        if (JSON.parse(data).status == "wait"){
          document.getElementById("qrmsg2").innerHTML = "cheking....";
        }else if(JSON.parse(data).status == "get"){
          document.getElementById("qrmsg2").innerHTML = "finish";
          document.getElementById("qr2").innerHTML = "";
          console.log(data);
          let id_token = JSON.parse(data).token.id_token;
          let vp_token = JSON.parse(data).token.vp_token;
          document.getElementById("result1").innerHTML = "<b>[token]:</b><br><b>ID token</b>:<br><blockquote>"+JSON.stringify(id_token)+"</blockquote>";
          document.getElementById("result2").innerHTML = "<b>VP_token</b>:<br><blockquote>"+JSON.stringify(vp_token)+"</blockquote>";
          clearInterval(clearid);
        }
      }
    );
  }
</script>
<script>
  // Source code: https://codepen.io/ocinpp/pen/EpbXKz

  // reference to the current media stream
  var mediaStream = null;

  // Prefer camera resolution nearest to 1280x720.
  var constraints = {
    audio: false,
    video: {
      width: { ideal: 640 },
      height: { ideal: 480 },
      facingMode: "environment"
    }
  };

  async function getMediaStream(constraints) {
    try {
      mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
      let video = document.getElementById('cam');
      video.srcObject = mediaStream;
      video.onloadedmetadata = (event) => {
        video.play();
      };
    } catch (err) {
      console.error(err.message);
    }
  };

  async function switchCamera(cameraMode) {
    try {
      // stop the current video stream
      stopVideoStream();

      // Show the video element 
      document.getElementById('cam').style.display = '';

      // change "facingMode"
      constraints.video.facingMode = cameraMode;

      // Show the snap button and hide the start video button
      document.getElementById('snapBtn').style.display = '';
      document.getElementById('switchFrontBtn').style.display = 'none';

      // Hide the selfie image
      document.getElementById('selfie').style.display = 'none';

      // get new media stream
      await getMediaStream(constraints);
    } catch (err) {
      console.error(err.message);
      alert(err.message);
    }
  }

  function takePicture() {
    let canvas = document.getElementById('canvas');
    let video = document.getElementById('cam');
    let selfie = document.getElementById('selfie');
    let context = canvas.getContext('2d');

    const height = video.videoHeight;
    const width = video.videoWidth;

    if (width && height) {
      canvas.width = width;
      canvas.height = height;
      context.drawImage(video, 0, 0, width, height);
      var data = canvas.toDataURL('image/jpeg');
      selfie.setAttribute('src', data);

      // Hide the video
      document.getElementById('cam').style.display = 'none';

      // Hide the snap button and show the start video button
      document.getElementById('snapBtn').style.display = 'none';
      document.getElementById('switchFrontBtn').style.display = '';

      // Show the image and the remove button
      document.getElementById('selfie').style.display = '';
      document.getElementById('clearBtn').style.display = '';

      stopVideoStream();

      // Get the image URL in base64 format
      getImageULR();

    } else {
      clearPhoto();
    }
  }

  function stopVideoStream() {
    // Stop the current video stream
    if (mediaStream != null && mediaStream.active) {
      var tracks = mediaStream.getVideoTracks();
      tracks.forEach(track => {
        track.stop();
      })
    }

    // set the video source to null
    document.getElementById('cam').srcObject = null;
  }

  function clearPhoto() {
    let canvas = document.getElementById('canvas');
    let selfie = document.getElementById('selfie');
    let context = canvas.getContext('2d');

    context.fillStyle = "black";
    context.fillRect(0, 0, canvas.width, canvas.height);
    var data = canvas.toDataURL('image/jpeg');
    selfie.setAttribute('src', data);

    // Hide the remove button
    document.getElementById('clearBtn').style.display = 'none';
  }
  function getImageULR(){
    var indexOfBase64 = document.getElementById("selfie").src.indexOf("base64,");
    document.getElementById("imageUploadStr").value = document.getElementById("selfie").src.substring(indexOfBase64 + 7);
  }

  clearPhoto();
</script>
</html>
```
</details>

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

``` package.json
{
  "dependencies": {
    "did-jwt": "^8.0.0",
    "elliptic": "^6.5.4",
    "express": "^4.18.2",
    "qrcode": "^1.5.3",
    "uuid": "^9.0.1",
    "web-did-resolver": "^2.0.27"
  }
}
```
</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');

app.use(bodyParser.json({ extended: true, limit: '10mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
app.use(express.json({ extended: true, limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use('/', express.static(__dirname + '/views/'));
app.use(bodyParser.text({type: '*/*'}));
app.use(bodyParser.raw({type: '*/*'}));

//*******setting********
const port=8888;
let hostname = '127.0.0.1'+":"+port;
const pinenable=false;
const method="self" // "attest"
if (process.argv.length > 2){ hostname = process.argv[2];}
// authentication by azure directory
const directory = "https://login.microsoftonline.com/<Tenant id>/v2.0";
//**********************

//*******global variable*******
let DID="did:web:"+hostname;
let task= new Array();
let taskstatus= new Array();
let storageJson=[];
let didweb, didwebkey;
const main = async()=>{
  didwebkey = await init(hostname);
  didweb = didjson(DID, didwebkey, hostname)
}
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] =="well-known" && cmd[1] == "did.json"){
    console.log("access to did.json")
    res.send(didweb);

  } else if (cmd[0] =="well-known" && cmd[1] == "did-configuration.json"){
    console.log("access to did-configuration.json")
    res.send(await makedidconfig());

  } else if (cmd[0] == "issuanceRequests"){
    console.log("start session:"+cmd[1]+", issuanceRequests")
    taskstatus[cmd[1]].vcstatus = "request_retrieved";
    res.send(await issuevcrequest(task[cmd[1]]));

  } else if(cmd[0] == "contracts" && cmd[2] == "manifest"){
    console.log("manifest for :"+cmd[1]);
    res.send(await makemanifest(task[cmd[1]],method));

  } else if (cmd[0] =="istatus"){
    ret={"status":taskstatus[cmd[1]].vcstatus};
    if (taskstatus[cmd[1]].vcstatus =="request_retrieved"){
      if (pinenable && (method !="attest")){
         ret.pin=task[cmd[1]].pin;
      }
    }
    res.send(JSON.stringify(ret));
  } else if (cmd[0] =="callQR"){
    let [redirecturi, sessNo] = await verifierInit(req.query);

    inviteUrl = "openid-vc://?request_uri="+redirecturi;
    QRCode.toDataURL(inviteUrl, function (err, url) {
      let sendData = {"qrcode":url, "sess":sessNo, "url":redirecturi};
      res.send(sendData);
    });
  } else if (cmd[0]== "presentationRequests"){
    console.log(cmd[1])
    if (typeof task[cmd[1]] !=="undefined"){
      console.log("VC request push");
      let jwt = await presenvcrequest(task[cmd[1]]);
      console.log(`//// JWT:\n${jwt}`)
      const decoded = didJWT.decodeJWT(jwt)
      console.log('\n//// JWT Decoded:\n',decoded)
      res.send(jwt);
    }else{
      rese.send("");
    }
  } else if (cmd[0]=='vstatus'){
    let ret={};
    if (taskstatus[cmd[1]].vp !=""){
      taskstatus[cmd[1]].vcstatus = "get";
      let tokenraw = taskstatus[cmd[1]].vp;
      let token ={}
      token.id_token = didJWT.decodeJWT(tokenraw.id_token);
      delete token.id_token["data"]
      token.vp_token = didJWT.decodeJWT(tokenraw.vp_token);
      delete token.vp_token["data"]
      ret.token = token;
    }
    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 [redirecturi, sessNo] = await issuerInit(req.body);
    let inviteUrl = "openid-vc://?request_uri=" + redirecturi;
    QRCode.toDataURL(inviteUrl, function (err, url) {
      let sendData = {"qrcode":url, "sess":sessNo, "url":redirecturi};
      res.send(sendData);
    });

  }else if (cmd[0] == "issue"){
    console.log("post from ms authenticator:")
    let decoded = didJWT.decodeJWT(req.body);
    let token;
    if (method == "attest"){
      token = decoded.payload.attestations.accessTokens[directory]
    }else{
      token = decoded.payload.attestations.idTokens["https://self-issued.me"]
    }
    let sess = cmd[1];
    if ((pinenable && decoded.payload.pin == task[sess].extpin) || (method=="attest")||!pinenable){
      let ret = await createVc(decoded, task[sess]);
      if (task[sess].iss=="did"){
        await saveDid(task[sess].issdid, task[sess].isskey);
      }
      res.send(JSON.stringify(ret));
    }else{
      console.log("pin is wrong, return error!");
      let sendData = {
        "requestId":"","date":"","mscv":"",
        "error":{
        "code":"unauthorized",
        "message":"The requested resource requires authentication",
          "innererror":{
            "code":"Unauthorized",
            "message":"An unhandled error has occurred verifying a token."
          }
        }
      };
       res.send(JSON.stringify(sendData));
    }
  }else if (cmd[0] == "completeIssuance"){
    // server   <--- ms authenticator
    if (req.body.code == 'issuance_successful'){
      console.log("vc issue is success");
    }else{
      console.log("vc issue is failed");
    }
    console.log("state:"+req.body.state);
    taskstatus[req.body.state].vcstatus="issuance_successful";
    res.status(202).send('OK')
  }else if (cmd[0]=="presentationRequests"){
    if (typeof taskstatus[cmd[1]] !=="undefined"){
      taskstatus[cmd[1]].vp = req.body
      let id_token = req.body.id_token;
      let vp_token = req.body.vp_token;

      let decoded = didJWT.decodeJWT(id_token);
      console.log('\n//// id_token:\n',JSON.stringify(decoded))
      decoded = didJWT.decodeJWT(vp_token);
      console.log('\n//// vp_token:\n',JSON.stringify(decoded))
      let vc=decoded.payload.vp.verifiableCredential[0];
      decoded = didJWT.decodeJWT(vc);
      console.log('\n//// vc:\n',JSON.stringify(decoded))
      res.status(202).send('OK')
    }else{
      res.status(500);
    }
  }else{
    res.send(null);
  }
});


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

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, , _document] = await createDid("web", _hostname);
    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 createDid(_method, _hostname){
  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')
  };
  console.log("--------generate key for did -------------------")
  console.log(` d : ${prv.getPrivate().toBuffer().toString('base64')}`);
  console.log(` x (b64): ${pub.x.toBuffer().toString('base64')}`);
  console.log(` y (b64): ${pub.y.toBuffer().toString('base64')}`);
  console.log("-----------------------------\n")
  let _key={"publicJwk":ecjwk,"privateJwk":ecjwkpri};
  let _document
  let _did
  if (_method == "web"){
    // web method
    _did = "did:web"+_hostname;
    _document =didjson(_did, _key, _hostname);
    console.log("-> "+JSON.stringify(_document));
  }
  return [_key, _did, _document];
}

function createSigner(_method, _key){
  let _signer = didJWT.ES256KSigner(didJWT.hexToBytes(Buffer.from(didwebkey.privateJwk.d, 'base64').toString('hex')))
  return _signer;
}


/////////////////////////
//   did:web
/////////////////////////
function didjson(_did, _key, _hostname){
  let _document = {
    "id":"",
    "@context":["https://www.w3.org/ns/did/v1",{"@base":""}],
    "service":[
      {"id":"#linkeddomains","type":"LinkedDomains","serviceEndpoint":{"origins":""}}
    ],
    "verificationMethod":[
      {"id":"#owner","controller":"",
        "type":"EcdsaSecp256k1VerificationKey2019",
        "publicKeyJwk":{"crv":"secp256k1","kty":"EC","x":"","y":""}
      }
    ],
    "authentication":["#owner"],"assertionMethod":["#owner"]
  };
  _document.id = _did;
  _document["@context"][1]["@base"]=_did;
  _document.service[0].serviceEndpoint.origins = ["https://"+_hostname+"/"];
  _document.verificationMethod[0].controller = _did;
  _document.verificationMethod[0].publicKeyJwk = _key.publicJwk;
  return _document;
}

async function makedidconfig(){
  let didconfig = {
    "@context":"https://identity.foundation/.well-known/contexts/did-configuration-v0.0.jsonld", 
    "linked_dids":[]
  };
  let configPayload = {
    "sub":didweb.id,
    "iss":didweb.id,
    "nbf":1674647331,
    "vc":{
      "@context":[
        "https://www.w3.org/2018/credentials/v1",
        "https://identity.foundation/.well-known/contexts/did-configuration-v0.0.jsonld"
      ],
      "issuer":didweb.id,
      "issuanceDate":"2023-01-25T11:48:51.998Z",
      "expirationDate":"2048-01-25T11:48:51.998Z",
      "type":["VerifiableCredential","DomainLinkageCredential"],
      "credentialSubject":{"id":didweb.id,"origin":didweb.service[0].serviceEndpoint.origins[0]}
    }
  };

  let signer = didJWT.ES256KSigner(didJWT.hexToBytes(Buffer.from(didwebkey.privateJwk.d, 'base64').toString('hex')))
  const configjwt = await didJWT.createJWT(
    configPayload,
      { issuer:DID,expiresIn:3600,signer },
      { alg: 'ES256K', kid: DID+"#owner" }
  )
  didconfig.linked_dids[0]=configjwt;
  return didconfig;
}

////////////////////////////
//  vc issuer
/////////////////////////////

async function issuerInit(_body){
  let sessNo = crypto.randomBytes(8).toString("hex");
  let serviceEndpoint = "https://"+hostname+"/";

  task[sessNo]= {
    "sess":sessNo,
    "userid":crypto.randomBytes(32).toString("base64"),
    "clientName":_body.clientName,
    "type":_body.type,
    "claims":_body.claims,
    "photo":_body.photo,
    "serviceEndpoint":serviceEndpoint,
    "isskey": didwebkey,
    "issdid": DID
  };

  if (pinenable){
    task[sessNo].pin = Math.random().toString(10).slice(-6);
    task[sessNo].pinsalt = crypto.randomBytes(32).toString("hex");
    task[sessNo].pinhash = "qXp47rTIcPN2rfAz/sCKDAVNgFzKG7bTzVnCjWg5suU=";
    let pinsha256 = crypto.createHash('sha256');
    pinsha256.update(task[sessNo].pinsalt+task[sessNo].pin);
    task[sessNo].extpin = pinsha256.digest('base64');
  }
  let redirecturi = task[sessNo].serviceEndpoint+".issuanceRequests/"+sessNo;
  taskstatus[sessNo]={"vcstatus":"wait"};
  return [redirecturi, sessNo];
}

async function issuevcrequest(_param){
  let hintbody ={
    "id":uuid.v4(),
    "sub":_param.userid,
    "aud":_param.serviceEndpoint+".issue/"+_param.sess,
    "nonce":crypto.randomBytes(16).toString("base64"),
    "sub_jwk":{
      "crv":"secp256k1",
      "kid":_param.issdid+"#owner",
      "kty":"EC",
      "x":_param.isskey.publicJwk.x,
      "y":_param.isskey.publicJwk.y
    },
    "did":_param.issdid,
    "type":_param.type,
    "credentialSubject":{
      "lastName":_param.claims.lastName,
      "firstName":_param.claims.firstName
    },
    "iss":"https://self-issued.me",
    "jti":uuid.v4().toUpperCase()
  };
  const signer = createSigner(_param.iss, _param.isskey);
  const hint_jwt = await didJWT.createJWT(
    hintbody,
    { issuer:"https://self-issued.me", expiresIn:3600, signer },
    { alg: 'ES256K', kid: _param.issdid+"#owner" }
  );
  if (pinenable){
    hintbody.pin = {"length":6,"type":"numeric","alg":"sha256","iterations":1,"salt":_param.pinsalt, "hash":_param.pinhash};
  }
  if (typeof _param.photo !=="undefined"){
    console.log("add a photo");
    hintbody.credentialSubject.photo = _param.photo
  }
  let isbody ={
    "jti":uuid.v4().toUpperCase(),
    "response_type":"id_token",
    "response_mode":"post",
    "scope":"openid",
    "nonce":crypto.randomBytes(16).toString("base64"),
    "client_id":_param.issdid,
    "redirect_uri":_param.serviceEndpoint+".completeIssuance",
    "prompt":"create",
    "state":_param.sess,
    "registration":{
      "client_name":_param.clientName,
      "subject_syntax_types_supported":["did:ion"],
      "vp_formats":{
        "jwt_vp":{"alg":["ES256K"]},"jwt_vc":{"alg":["ES256K"]}
      }
    },
    "claims":{
      "vp_token":{
        "presentation_definition":{
          "id":"b1cb4efd-8e33-482f-8dad-4452cbc2a8d1",
          "input_descriptors":[{
             "id":"Sample",
             "schema":[{"uri":"Sample"}],
             "issuance":[{"manifest":_param.serviceEndpoint+".contracts/"+_param.sess+"/manifest"}]
          }]
        }
      }
    },
    "id_token_hint":hint_jwt
  };
  if (pinenable){
    isbody.pin = {"length":6,"type":"numeric","alg":"sha256","iterations":1,"salt":_param.pinsalt, "hash":_param.pinhash};
  }
  const jwt = await didJWT.createJWT(
    isbody,
    { issuer:_param.issdid+"#owner", expiresIn:3600, signer },
    { alg: 'ES256K', kid: _param.issdid+"#owner" }
  )
  return jwt;
}

async function createVc(_decoded, _param){
  let idToken
  if (typeof _decoded.payload.attestations.idTokens !=="undefined"){
    idToken = _decoded.payload.attestations.idTokens["https://self-issued.me"];
  }else{
    idToken = _decoded.payload.attestations.accessTokens[directory];
  }
  console.log("\n-> idToken:");
  const subject = didJWT.decodeJWT(idToken).payload
  let vcbody =  {
    "jti": _decoded.payload.jti,
    "vc": {
      "@context": ["https://www.w3.org/2018/credentials/v1"],
      "type": ["VerifiableCredential", "StudentCard"],
      "credentialSubject": {},
      "exchangeService": {
        "id": _param.serviceEndpoint+".card/exchange",
        "type": "PortableIdentityCardServiceExchange2020"
      },
      "sub": _decoded.header.kid
    }
  };
  if (method=="attest"){
    vcbody.vc.credentialSubject.lastName = subject.family_name;
    vcbody.vc.credentialSubject.firstName = subject.given_name;
  }else{
    vcbody.vc.credentialSubject = subject.credentialSubject;
  }
  if (typeof _param.photo !=="undefined"){
    console.log("add photo")
    vcbody.vc.credentialSubject.photo = _param.photo.split("base64,")[1];
  }
  vcbody.vc.credentialStatus = await makeCredStatus(vcbody.vc, _param.issdid, new Date().toISOString());
  let signer = createSigner(_param.iss, _param.isskey);
  const jwt = await didJWT.createJWT(
    vcbody,
    { issuer:_param.issdid, expiresIn:3600, signer },
    { alg: 'ES256K', kid: _param.issdid+"#owner" }
  )
  const decoded = didJWT.decodeJWT(jwt)
  console.log('\n//// JWT Decoded:\n',decoded)
  let sendData = {"vc":jwt}
  return sendData;
}

async function makeCredStatus(_vc, _iss, _date){
  let credStatus = {
      "id": "https://xxxx.github.io/credential-status/3BOJ1LAFUS#26",
      "type": "StatusList2021Entry",
      "statusPurpose": "revocation",
      "statusListIndex": 26,
      "statusListCredential": "https://xxxx.github.io/credential-status/3BOJ1LAFUS"
    }
  return credStatus;
}

async function makemanifest(_param, _method){

  let manifestbody = {
    "id": _param.sess,
    "display": {
      "locale": "en-US",
      "contract": _param.serviceEndpoint+".contracts/"+_param.sess+"/manifest",
      "card": {
        "title": "ID card",
        "issuedBy": "ngrok regitration",
        "backgroundColor": "#ffffff",
        "textColor": "#000000",
        "logo": {
          "uri": "https://didcustomerplayground.blob.core.windows.net/public/VerifiedCredentialExpert_icon.png",
          "description": "Verified skill Card"
        },
        "description": "Use your verified credential to prove to anyone that you know all about verifiable credentials."
      },
      "consent": {
        "title": "Do you want to get your Verified Credential?",
        "instructions": "Sign in with your account to get your card."
      },
      "claims": {
        "vc.credentialSubject.firstName": {"type": "String","label": "first Name"},
        "vc.credentialSubject.lastName": {"type": "String","label": "last Name"},
      },
      "id": "display"
    },
    "input": {
      "credentialIssuer": _param.serviceEndpoint+".issue/"+_param.sess,
      "issuer": DID,
      "id": "input"
    }
  };
  if (typeof _param.photo!=="undefined"){
    manifestbody.display.claims["vc.credentialSubject.photo"]={"type": "image/jpg;base64url","label": "User picture"};
  }

  if (_method == "attest"){
    manifestbody.input.attestations = {
      "accessTokens": [{
        "id": directory,
        "encrypted": false,
        "claims": [],
        "required": true,
        "configuration": directory,
        "resourceId": "bb2a64ee-5d29-4b07-a491-25806dc854d3",
        "oboScope": "User.Read.All" 
      }]
    };
  }else{
    manifestbody.input.attestations = {
      "idTokens": [{
        "id": "https://self-issued.me",
        "encrypted": false,
        "claims": [{
          "claim": "$.lastName",
          "required": true,
          "indexed": false
        },{
          "claim": "$.firstName",
          "required": true,
          "indexed": false
        }],
        "required": true,
        "configuration": "https://self-issued.me",
        "client_id": "",
        "redirect_uri": ""
      }]
    };
  }
  if (typeof _param.photo!=="undefined"){
    manifestbody.input.attestations.idTokens[0].claims.push({"claim": "photo","required": false,"indexed": false});
  }
  let signer = createSigner("web", didwebkey);
  const jwt = await didJWT.createJWT(
     manifestbody,
     { issuer:DID, expiresIn:3600, signer },
     { alg: 'ES256K', kid: DID+"#owner" }
  )
  const decoded = didJWT.decodeJWT(jwt)
  return JSON.stringify({"token":jwt})
}


//////////////////
/// verifier
/////////////////
async function verifierInit(_query){
  let sessNo = crypto.randomBytes(8).toString("hex");
  let serviceEndpoint = "https://"+hostname+"/";

  task[sessNo]= {
    "sess":sessNo,
    "userid":crypto.randomBytes(32).toString("base64"),
    "clientName":_query.clientName,
    "type":_query.type,
    "serviceEndpoint":serviceEndpoint,
    "isskey": didwebkey,
    "issdid": DID
  };

  let redirecturi = task[sessNo].serviceEndpoint+".presentationRequests/"+sessNo;
  taskstatus[sessNo]={"vcstatus":"wait", vp:""};
  return [redirecturi, sessNo];
}

async function presenvcrequest(_param){
  let payload = {
    "jti":uuid.v4(),
    "response_type":"id_token",
    "response_mode":"post",
    "scope":"openid",
    "nonce":crypto.randomBytes(16).toString("base64"),
    "client_id":"",
    "redirect_uri":"",
    "state":"",
    "registration":{
      "client_name":"839 home",
      "subject_syntax_types_supported":["did:ion", "did:key", "did:jwk"],
      "vp_formats":{
        "jwt_vp":{"alg":["ES256K","EdDSA"]},
        "jwt_vc":{"alg":["ES256K","EdDSA"]}
      }
    },
    "claims":{
      "vp_token":{
        "presentation_definition":{
          "id":uuid.v4(),
          "input_descriptors":[{
            "id":"Any student card",
            "purpose":"So we can see accept any studentCard scheme",
            "schema":[{"uri":"StudentCard"}],
          },{
            "id": uuid.v4(),
            "name": "LivenessCheck",
            "schema": [{
              "uri": "LivenessCheck"
            }]
          }]
        }
      }
    }
  };

  payload.client_id=DID;
  payload.redirect_uri = "https://"+hostname+"/.presentationRequests/"+_param.sess
  payload.state = "123456789";
  const signer = createSigner(_param.iss, _param.isskey);

  let jwt = await didJWT.createJWT(
    payload,
    { issuer:DID+"#owner", expiresIn:3600, signer },
    { alg: 'ES256K', kid: DID+"#owner" }
  )
  return jwt;
}
```
</details>

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

 this did:web:57dd-180-53-77-xxx.ngrok-free.app's key not registered
--------generate key for did -------------------
 d : 0tr5cu9T9f6Xz0H6Mwv0gwGM3NxSGTAzcOMcHS/sRnk=
 x (b64): 0G5yn2kRKSsPWcWIPQCX9YFJY0k4tOlucU4XR932VA0=
 y (b64): LCk2FkTpjLpvhnfWGOb+sn0drRof2aJl56VFDp8pook=
-----------------------------

-> {"id":"did:web57dd-180-53-77-xxx.ngrok-free.app","@context":["https://www.w3.org/ns/did/v1",{"@base":"did:web57dd-180-53-77-202.ngrok-free.app"}],"service":[{"id":"#linkeddomains","type":"LinkedDomains","serviceEndpoint":{"origins":["https://57dd-180-53-77-202.ngrok-free.app/"]}}],"verificationMethod":[{"id":"#owner","controller":"did:web57dd-180-53-77-xxx.ngrok-free.app","type":"EcdsaSecp256k1VerificationKey2019","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0G5yn2kRKSsPWcWIPQCX9YFJY0k4tOlucU4XR932VA0=","y":"LCk2FkTpjLpvhnfWGOb+sn0drRof2aJl56VFDp8pook="}}],"authentication":["#owner"],"assertionMethod":["#owner"]}
```
#### 5. ブラウザで上記```https://57dd-180-53-77-xxx.ngrok-free.app/```にアクセス
1. 下記画面が表示されるので、take a selfieボタン、その次に現れるsnapボタンでVCに張り込む顔写真を決定![web.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/88d63ec4-7054-f028-633d-94f33bc7f766.png)

1. 発行ボタンを押すとQRコードが表示されるので、それをスマホでスキャンして顔写真入りVCをスマホに保存する。
1. 検証の際は、画面上にある「資格の検証」ボタンを押してQRコードを表示
1. 表示されたQRコードを、スマホでスキャンすると、verifierとのVC共有の手続きが始まる