はじめに
VC(verifiable credential)を試してみようと考えて、スマホの中に入っているアプリの中で一番手軽そうにみえたMicrosoft Authenticatorに手を出したというのが、いじるきっかけ。またMS Authenticatorを取り扱っている記事が多くあるのも魅力の1つ。例えば以下のサイトでは、VCを発行するやりとりが詳しく解説されている。
ただ多くの記事にあるように、AzureのApp serviceでmy webサーバを立ち上げたり、.netを使ってVCを発行するアプリを作るのは、結構敷居が高い。
簡単に試す事ができるように、nodejsだけでms autheticatorにvcを発行できるようコードを書き起こした。ちょうど下の図にあるAPI群をもったコードである。
いじってみた結果
- パターンA: 何もいじらない。
- 「追加ボタンを押すとVCがスマホに入る」
- パターンB: ④vc issue requestにpinというフィールドを入れる。
- 「確認コードボタンが表示され、入力したあとサイトAでのPIN検証後に、VCがスマホに入る」
- パターンC: ⑥manifestをいじって、"idTokens"ではなく"accessTokens"にする。
1. いじった箇所(パターンB)
vc issuer requestにpinフィールドを設けることで、MS authenticatorにpin入力を促すことができる。仕様はhttps://learn.microsoft.com/en-us/entra/verified-id/issuance-request-apiに公開されているが、あまり詳しくない。実際には仕様にないhashなるものが送られていたりする。(※ただ調査してみると、hashはPIN認証の際に使われていないので、実質仕様どおりかもしれない。)
{
"jti":uuid.v4().toUpperCase(),
...
"claims":{},
"id_token_hint":hint_jwt
"pin":{"length":6,"type":"numeric","alg":"sha256","iterations":1,"salt":xxx, "hash":zzz}
...
};
ここでrequestすると、MS authentocatorはユーザが入力したpinの値をもとに base64( sha256(salt+pin) )を計算し、⑦の処理の際に、その値をissue/..に返してくる。
2. いじった箇所(パターンC)
- manifestをいじる前に、A guided tour of Microsoft Entra Verified IDにあるように、Azure上にtenantを作って、そこにユーザを作り、そのユーザとMS authenticatorを結びつけておく必要がある。
- create your Azure AD tenant(p23)
- create a test user(p25)
- Set up the user for Microsft Authenticator(p26)
結びつけられたら、そのtenant idをconst directory="https://login.microsoftonline.com/<tenant id>/v2.0"
に設定する。
2. あとは送信するmanifestをidTokensからaccessTokensに変更する。
- 変更前のmanifest
{
...
"display": {
"locale": "en-US",
...
},
"consent": {},
"claims": {},
},
"id": "display"
},
"input": {
"issuer": DID,
"id": "input",
attestations:{
"idTokens": [{
"id": "https://self-issued.me",
"encrypted": false,
"claims": [{..},{..}],
"configuration": "https://self-issued.me",
...
}]
}
}
}
- MS authenticatorにサインインを促すために、menifestに変更を加える部分
attestations = {
"accessTokens": [{
"id": directory,
"encrypted": false,
"claims": [],
"required": true,
"configuration": directory,
"resourceId": "bb2a64ee-5d29-4b07-a491-25806dc854d3",
"oboScope": "User.Read.All"
}]
};
- この変更を加えて、manifestを送ると、⑦でM authenticatorから入力があるときに、Azure DirectoryからのTokenをもらうことができる(※この値で、発行しようとしているVC内容のダブルチェックも可能)
{
"typ": "JWT",
"alg": "RS256",
"x5t": "T1St-dLTvyWRgxB_676u8krXS-I",
"kid": "T1St-dLTvyWRgxB_676u8krXS-I"
}.{
"aud": "bb2a64ee-5d29-4b07-a491-25806dc854d3",
"iss": "https://sts.windows.net/<tenant id>/",
"iat": 1701503608,
"nbf": 1701503608,
"exp": 1701508690,
"acr": "1",
"aio": "ATQAy/8VAAAAcJbmKE6C0R6TG2Fk7KpIMk+pah+RFU0Vd3QZWthaXOtyC+n6aAVkEuDb7gdbzvIY",
"amr": [
"pwd"
],
"appid": "4813382a-8fa7-425e-ab75-3b753aab3abb",
"appidacr": "0",
"family_name": "afo afo man",
"given_name": "hoge hoge",
"ipaddr": "180.53.77.202",
"name": "Test User 01",
"oid": "7194b49b-8a45-4dac-80f7-4dc67dae8892",
"puid": "100320031DE1E391",
"rh": "0.AWoAJ1cUGcbFuEWSo2otvxZB--5kKrspXQdLpJElgG3IVNNqAEk.",
"scp": "user_impersonation",
"sub": "87sg7Rre57ZujnBSn4i5OpRV4tN6fzss-Q6vn90JMB8",
"tid": "<tenant id>",
"unique_name": "testuser1@xxx.onmicrosoft.com",
"upn": "testuser1@xxx.onmicrosoft.com",
"uti": "-gO-2Ta2f0CswFcXtscCAA",
"ver": "1.0"
}.signature
動作確認
1. index.html, server.js, package.jsを以下のフォルダ構成のように配置する
├── server.js
├── package.json
└── views/
├── index.html
index.htmlのコードを見る
<!DOCTYPE html>
<html lang='en'>
<head><meta charset='utf-8'></head>
<body>
<div align=center>
<div id="title">[ngrok registration]</div>
<div id="parameter">given name:<input type="text" id="given" value="hogehoge"><br>
family name:<input type="text" id="family" value="deadbeaf"><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 registration",
"type": "ngrokVerifiedCredential",
"claims": {"lastName": document.getElementById('family').value, "firstName": document.getElementById('given').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のコードを見る
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());
app.use(bodyParser.urlencoded({ extended: true }));
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("====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 makevcrequest(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{
res.send(ret);
}
});
app.post('/.*', async function(req,res){
console.log("\n====POST====");
console.log("cmd:"+JSON.stringify(req.params));
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{
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,
"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 makevcrequest(_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};
}
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"],
"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;
}
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 (_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": ""
}]
};
}
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})
}
package.jsonのコードを見る
{
"dependencies": {
"consolidate": "^0.16.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"did-jwt": "^6.11.0",
"did-resolver": "^4.0.1",
"elliptic": "^6.5.4",
"express": "^4.18.2",
"jose": "^4.11.2",
"ngrok": "^4.3.3",
"qrcode": "^1.5.1",
"web-did-resolver": "^2.0.21"
}
}
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: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/にアクセス
- 画面上にある「発行」ボタンを押してQRコードを表示
- 表示されたQRコードを、スマホでスキャンするとVC発行の手続きが始まる
6. いじる箇所
- pinを有効にする場合、server.jsの
const pinenable=false;
をconst pinenable=true;
に - ms loginを有効にする場合、server.jsの
const method="self";
をconst method="attest";
に。