LoginSignup
0
0

EntraのfaceCheckを体験してみる

Last updated at Posted at 2024-02-04

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

はじめに

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

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

 faceCheck機能はVerifiable Credential(vc)の中に顔写真を埋め込み、verifierサイトにログインするときに、そのVCの持ち主かどうかをVCに埋め込まれた顔写真で確認するものである。
face.png

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

faceCheckの流れ

1. スマホにVCを発行してもらう

Woodgrove Verified ID demo siteにアクセスして、「Student Card」を発行してもらう。発行画面は下記のような感じである。Optionであるが、「choose File」で手持ちの顔写真(*.webp)を選択すると下記のような登録画面になる。
reg1.png

このときスマホに発行されたVCを見てみると下記のようになっている。claimの中にphotoという項目が増え、そこにはwebpフォーマットの接頭語が見えるので、登録時にuploadした顔写真のraw dataをそのまま放り込んでいる感じである。

{
 "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

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

2.2 faceCheck

スキャンすると、MS authenticatorアプリは、redirect_uriに示されたhttps://5070-153-132-214-182.ngrok-free.app/.presentationRequests/ebab5252be665477からrequestを読み出し、そのrequestに書かれた内容に従って、以下のような画面遷移を起こす。
face1.png

MS Authenticatorが読み取ったrequestは、以下のような内容である。このrequestの中でfaceCheck特有の値はinput_descriptorsに書かれている{"uri": "LivenessCheck"}である。これが指定されることでMS authenticatorは顔の読み取りを行うようだ。

{
  "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は下記内容である。

{ 
  id_token:"eyJ0eXAiOiJ....UzI1NksiLCJraWQiOiJkaWQ6aW9uOkVpQW42WXVlMDVZQUFDeFU4bUh0c....ZDRmYWIxLTU0ODktNGRlZi1hMDI0LTkwY2VkODM3MDZkZCIsImlkIjoiM0NDQjg1NDUtQkNGOS00QzFFLThGRTAtQjg0REIzNjE5MDU4In19fQ.1oACywpUGgkpLKQNGXCRLsiPVFlirBfs5MnihQSa401WpB24buqXCOgNz6NlSBa26fXjOZVibJRV_OL6RiWb6Q",
  vp_token: "eyJraWQiOiJ.....BvMFNXcHZhVlI2YURSVU1tOTRXVEE1VmxaSGNHRmtlbXhWVkd0b05rNTZSVFJQUjBabVQxZE9VVmRVVFhSUmEzaEtXa1ZzV0dFd2VFZFpNRkl4VFVOSmMwbHVhMmxQYVVrelYydHZNRlV5UmtKT1Z",
  state: "123456789"
}

この中のvp_tokenの中身を詳しく見ていくと、以下のように2つのVC1, VC2が入っていることがわかる。VC1は上記で紹介したClaimにphotoが入ったstudentCard VCである。

{
    "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

顔チェックの生体認証ビジョン チェックはモバイル デバイスで実行されるのですか?
いいえ。 写真とキャプチャされたライブネス データの間の生体認証チェックは、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
index.htmlのコードを見る
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>
package.jsonのコードを見る
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"
  }
}
server.jsのコードを見る
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;
}

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

  2. 発行ボタンを押すとQRコードが表示されるので、それをスマホでスキャンして顔写真入りVCをスマホに保存する。

  3. 検証の際は、画面上にある「資格の検証」ボタンを押してQRコードを表示

  4. 表示されたQRコードを、スマホでスキャンすると、verifierとのVC共有の手続きが始まる

0
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
0
0