LoginSignup
0
0

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

Last updated at Posted at 2024-01-08

[2024/2/17 update]
credo-ts libは、openid-credential-offerに含まれる"credentials":["OpenBadgeCredential"]をもとにissuerのmetadataを検索する。なのでmetadataに含まれる値、すなわち"credentials":["OpenBadgeCredentialJwt"]に変更

はじめに

最近EUDI walletの話をよく聞くようになってきた。EUDIとは欧州デジタルIDウォレットのことらしい(参考)。IDウォレットではOpenID4VCI/OpenID4VP+SIOPv2を使って、VCの受け渡しをするようである。

 図にすると以下のような感じなる(引用元:globalPlatform)
eudi.png

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

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

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

OpenID4VCIの動き

1) QRコードを発行

 demoサーバにブラウザでアクセスし、[manually fill out details]を選択して、適当なfirstname/lastname, emailを入力してcontinueをクリック。その後select credentials画面が出るので「University degree」を選択すると、下記のようなQRコードが発行される
qr.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情報にkhttps://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
index.htmlのコードを見る
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>
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');
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
}
package.jsonのコードを見る
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"
  }
}

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コードを表示

  2. 表示されたQRコードを、スマホでスキャンするとVC発行の手続きが始まる
    sma.png

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