最近SD-JWTの話をよく聞くようになってきた。選択的開示のためのデータフォーマット([参考])らしい。以前試したEUDIで、SD-JWTを用いたSelective Disclosureの実現を必須としていることから注目されているようである。
「記事:Verifiable Credentialsの署名を壊さずに選択的開示を行うSD-JWTを体感してみる」によれば、Verifiable Credentailを発行するissuerが下記のようなcredentialを発行してくれるそうな。ユーザが選択開示できる対象が8個あれば、credential jwtのあとに8個分の~がつくようなフォーマットらしい。
eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MzU2ODk2NjEsIl9zZCI6WyI1R19rclp0TVRNUGx0QVdXTk1oZER4Sk10MTVTQTFLRmtkMmdJbFN2THRRIiwiNXA2YnQ1M0Q5cDRNZmtWWVFjaUw0cFdYQWtJanowUGJMWEdEME5IaWkydyIsIjZKdDVseHJlaVRLOG9sVFJ6eUhvSFlZUG11MEctWlZ2V3BhY1ZtWVlUOE0iLCJFdy1kS1JtQTJ1U2d3OUVtR3dOSWZRa3NQcFBJczBxQTRPRS04NkRRM19ZIiwiSHotQllQUUptSHNGbElmWl85Q0o2SWJvcWlzb1pOdHd6a2IwWjNKQWxGNCIsIkozUF9nTkEzTEVDSXpUdHRCdVg2MmNMVklDREZ6Ui1ycm9pQ1FrYkJidVEiLCJPMmJEVUhhRUxaSXN5T1hUeXV0V3VuQWdtMzdRVGtiQ1RSdzczYmk1NXhFIiwiUTR2dHJhRHZwcnFCa2pyUG9MV185RlpEZXRGS0tBR2N5YTA1ckl3cTR4ZyJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.LAlXQnPK8rOhxBhbg_FU-hiBJJqCX5CYWYTp0YcbbIxnBZPN1ZhRgE_SLh0rEg1L559GxmG1WrpGMyOl1rDoAA
~WyJ5OWVMRmI3azdXZnd4ZkllQWp5eUt3Iiwic3ViIiwiMjQ4Mjg5NzYxMDAxIl0
~WyJUNkVlZzRsaVJyN3VtOUZxOFRYekR3IiwibmFtZSIsIkphbmUgRG9lIl0
~WyJUUnRocWdUc29kZzVJbnBBR2ZvNjJnIiwiZ2l2ZW5fbmFtZSIsIkpvbmUiXQ
~WyJmRWx5VzFIdlVjX1dYTnFLeUdRcEJ3IiwiZmFtaWx5X25hbWUiLCJEb2UiXQ
~WyI1blgwRmZTNHUtVjN3aFNORUpPQndnIiwiZ2VuZGVyIiwiZmVtYWxlIl0
~WyJhSzhJYWc2dzB1Mno1WVpxMWlXOC13IiwiYmlydGhkYXRlIiwiMDAwMC0xMC0zMSJd
~WyIwR0pLdnN2aDNpbnFkeGlUM3MxbVJRIiwiZW1haWwiLCJqb25lZG9lQGV4YW1wbGUuY29tIl0
~WyJXMHpCa1lfLThqUVhNY3JnYTJTak9BIiwicGljdHVyZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyJd
この~で始まる部分はbase64でエンコードされた文字列だという話なので、それを明に書き下すと、
eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MzU2ODk2NjEsIl9zZCI6WyI1R19rclp0TVRNUGx0QVdXTk1oZER4Sk10MTVTQTFLRmtkMmdJbFN2THRRIiwiNXA2YnQ1M0Q5cDRNZmtWWVFjaUw0cFdYQWtJanowUGJMWEdEME5IaWkydyIsIjZKdDVseHJlaVRLOG9sVFJ6eUhvSFlZUG11MEctWlZ2V3BhY1ZtWVlUOE0iLCJFdy1kS1JtQTJ1U2d3OUVtR3dOSWZRa3NQcFBJczBxQTRPRS04NkRRM19ZIiwiSHotQllQUUptSHNGbElmWl85Q0o2SWJvcWlzb1pOdHd6a2IwWjNKQWxGNCIsIkozUF9nTkEzTEVDSXpUdHRCdVg2MmNMVklDREZ6Ui1ycm9pQ1FrYkJidVEiLCJPMmJEVUhhRUxaSXN5T1hUeXV0V3VuQWdtMzdRVGtiQ1RSdzczYmk1NXhFIiwiUTR2dHJhRHZwcnFCa2pyUG9MV185RlpEZXRGS0tBR2N5YTA1ckl3cTR4ZyJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.LAlXQnPK8rOhxBhbg_FU-hiBJJqCX5CYWYTp0YcbbIxnBZPN1ZhRgE_SLh0rEg1L559GxmG1WrpGMyOl1rDoAA
~base64encode(["y9eLFb7k7WfwxfIeAjyyKw","sub","248289761001"]) ---------1
~base64encode(["T6Eeg4liRr7um9Fq8TXzDw","name","Jane Doe"]) ------------2
~base64encode(["TRthqgTsodg5InpAGfo62g","given_name","Jone"]) ----------3
~base64encode(["fElyW1HvUc_WXNqKyGQpBw","family_name","Doe"]) ----------4
~base64encode(["5nX0FfS4u-V3whSNEJOBwg","gender","female"]) ------------5
~base64encode(["aK8Iag6w0u2z5YZq1iW8-w","birthdate","0000-10-31"]) -----6
~base64encode(["0GJKvsvh3inqdxiT3s1mRQ","email","jonedoe@example.com"])-7
~base64encode(["W0zBkY_-8jQXMcrga2SjOA","picture","http://example.com/janedoe/me.jpg"]) -8
ということらしい。
ユーザは隠したい情報、例えばgender、birthdateであれば、それに対応する5,6部分の~Wy..を消去して、相手に渡すことで選択開示が可能となる。本体のcredentialには~で始まるbase64でエンコードされた文字列のハッシュ値が埋め込まれているそうだ(墨塗り署名に似た仕掛けだなと感じた)
sd-jwt形式のvcを取得するまでの流れ
1. QRコードをスキャン
sd-jwtを体験できるissuerを探してみた。animo、waltと試したものの、まだsd-jwtへの対応は出来ておらず、authlete社のデモサイトが唯一sd-jwt形式のcredentialを発行してくれた(2024年1月時点)。
発行してもらうには、Login ID/Passwordの欄に、デモサイトの使い方ページに書かれた値を入れてsubmitボタンを押すだけでOKである。押すと下記のようなQRコードが表示される。
2. Credential Issuer Metadata を取得
QRコードを読み取ると、以下のようなCredential Offer情報が取得できる。
openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Ftrial.authlete.net%22%2C%22credential_configurations%22%3A%5B%22IdentityCredential%22%2C%22org.iso.18013.5.1.mDL%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22BrSjFNeIYQJkemFOAj4nk5RrDD2lo8lNgRnXLD-o540%22%7D%7D%7D
このCredential Offerをsphereon社のライブラリoid4vci-clientに読み込ませるとpreAuthorizedCodeや、token endpointを取得してくれる
#ライブラリの動作をみていると、.well-known/openid-credential-issuer、.well-known/oauth-authorization-server、.well-known/openid-configurationをチェックしてmetadataを生成している印象
import { CredentialOfferClient, MetadataClient,} from '@sphereon/oid4vci-client';
const initiationRequestWithUrl
= await CredentialOfferClient.fromURI(_initiationURI);
const metadata
= await MetadataClient.retrieveAllMetadataFromCredentialOffer(initiationRequestWithUrl);
console.log(initiationRequestWithUrl.preAuthorizedCode)
console.log(metadata.token_endpoint)
console.log(metadata.issuer)
3.tokenの取得
issuer metadataからtoken endpoint情報をもらい、そこにQRコード経由で取得したpreAuthorizedCodeを用いてaccess_tokenを取得する。このデモサイトからaccess_tokenを取得する場合には、application/x-www-form-urlencoded形式でpreAuthorizedCodeを送る必要がある。
const clientId = '218232426';
const tokenReq = "client_id="+clientId
+ "&grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code&pre-authorized_code="
+ initiationRequestWithUrl.preAuthorizedCode;
const accessTokenResponse = await fetch(metadata.token_endpoint, {
method: 'post',
body: tokenReq,
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
const accessToken = await accessTokenResponse.json();
4. VCの取得
VCを取得するには、metadata.credential_endpointに、さきほど取得したtokenをheaderにセットして依頼を行う。このとき
- vctを設定する(いままでは、credentialTypesを設定)
- formatをvc+sd-jwtに設定する(いままではjwt_vc_jsonを設定)。
function main(){
...
const proofJwt = await generateKeyProof ("ES256",clientId, accessToken.c_nonce, metadata.issuer)
const credentialReq = {
"format": "vc+sd-jwt", // jwt_vc_json
"vct": "https://credentials.example.com/identity_credential", // credentialTypes:..
"proof": {
"proof_type": "jwt",
"jwt":proofJwt
}
};
const credentialResponse = await fetch(metadata.credential_endpoint, {
method: 'post',
body: JSON.stringify(credentialReq),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+accessToken.access_token}
});
const credential = await credentialResponse.json();
console.log(credential);
...
}
async function generateKeyProof (_alg, _clientid, _nonce, _aud){
const { privateKey, publicKey } = await jose.generateKeyPair(_alg);
let ecjwk = await jose.exportJWK(publicKey);
let ecjwkpri = await jose.exportJWK(privateKey);
let _key={"publicJwk":ecjwk,"privateJwk":ecjwkpri};
let args = {
"payload":{
"iat":Math.floor(new Date().getTime() / 1000),
"iss":_clientid,
"nonce": _nonce,
"aud":_aud,
},
"header":{
"typ":"openid4vci-proof+jwt",
"alg":_alg,
"jwk":_key.publicJwk
}
}
const privateKeyObj = await jose.importJWK(ecjwkpri, _alg);
const jwt = await new jose.SignJWT({ ...args.payload })
.setProtectedHeader({...args.header})
.sign(privateKeyObj);
return jwt;
}
すると、以下のようなvc+sd-jwt形式のデータが返ってくる。
{
format: 'vc+sd-jwt',
credential: 'eyJraWQiOiJKMUZ3SlA4N0M2LVFOX1dTSU9tSkFRYzZuNUNRX2JaZGFGSjVHRG5XMVJrIiwidHlwIjoidmMrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJfc2QiOlsiN3V5c0ZxWWdTWDVnWmFocmh0dE9ZMXlxeGg2QUFqQ242eE1mSWdOVnZBSSIsIjh6ekJWaVc5cl8wbFY0UlJYY01rYVVCRUNlZHduYTBPY0lNdEl5UVdqc0EiLCJCeEpfU0hSZ0lDWHFyOENGd0xFZlQ2b0VVR3A0YktrRlBVazF5dnBuekNNIiwiR2s4bjQxcHZ4WndtOWNvZmtTX01BLXdrZjY2bXZPVUVFV0FWUGFaenliayIsIlE1Z3Bjc1hvSHVZa3h3VHdhVXZ4YUR3VFpiZHE5OW5qbjRjNEJfZXFaU1EiLCJWSnVrU2NGQWFxUVhaUVhLR041MVJrcWVRN3FBUDVOc1JRMkpHeGpGMFBnIiwiVm5MWGNqXzRONVdXclVhNXp4LWRPWHhOMXVFSG1JUXEwU3UyQmNZd2R2TSIsImtUcmFMajdsbGN4eGxreVdrUHV2Tm1QZDc1ZU1DTlNOM0dPelpoNnh4YkUiLCJtaG9oSGc0Sm5qQzZfbi1uVXIwbzFkdVlPb2xISGFuc0RjTU9WYnIycTZvIiwib1E0b1IxZURmc1JQR0FSeHpxNkNsa0ZSbzc5dkQyRWJ6cVNYVUNUTDhUbyIsInFHel9pRlNKLVJKbG96NGdBUDZqOW5sekpCYW9Id0JsS0I2LVBSVlBXd1EiLCJ2eFVPalJnTmh4Vk1Hb292MHVFck4zbUtxdUxKN2pCR1RtdHhEbjFPU0Z3Il0sInZjdCI6Imh0dHBzOi8vY3JlZGVudGlhbHMuZXhhbXBsZS5jb20vaWRlbnRpdHlfY3JlZGVudGlhbCIsIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly90cmlhbC5hdXRobGV0ZS5uZXQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJraWQiOiJRNFhHMEdnOVVDODVEcW9Zc3ZzX2RKTnFBVTJtMXliVEhQOW1GYVQzLW9RIiwieCI6IjFfVERIaHVvLXQ1WTZfaHBITFEzc3FmV0J3eGRlNDhpdHJDNllqaklVbWciLCJ5IjoiRkpxWmhsWVEzcVZmcl84NE13WWlRNmJBbDEtb1VzSkI5dHFMTXR2ZFhFMCJ9fSwiaWF0IjoxNzA1ODE5MjY4fQ.Hkgkh8ri8b-pQC0M-BJbPGcsFkC7iLT3plLcA-h-ubLEVa8gmdKfBlRYbqvoVV0DI8CWURrFFJ_Qbw8u-uMkXQ~WyIyVlo0M1dwSGVocllrLTN3QmZ5d3l3Iiwic3ViIiwiMTAwNCJd~WyJGa3VINk04amkwLXNodVRQWWRaRUVRIiwiZ2l2ZW5fbmFtZSIsIkluZ2EiXQ~WyJoRzUtdjlyU2FPVEczdC1mZ2lYVHJ3IiwiZmFtaWx5X25hbWUiLCJTaWx2ZXJzdG9uZSJd~WyJERGJhcXBpeUtmYXdSeTNONUpCUXp3IiwiYmlydGhkYXRlIiwiMTk5MS0xMS0wNiJd~',
c_nonce: 'gTTnGBUukwkVm1GfiLFBu4W5tEGRIfO6_ytzF9iZePg',
c_nonce_expires_in: 86399
}
5. sd-jwtのパース
ライブラリ@eengineer1/sd-jwt-ts-node
にあるparse関数に上記credentialを流し込めば、いい感じに分析してくれる
import { SDJwt } from '@eengineer1/sd-jwt-ts-node'
async function sdJwtParse(sdPayload){
const sdJwt = SDJwt.parse(sdPayload)
const parse = {undisclosure:sdJwt.sdPayload.undisclosedPayload, disclosure:sdJwt.fullPayload}
console.log(JSON.stringify(parse, null,1))
return parse
}
上記関数に、引数credential値を入れて実行すると、以下のように情報開示をする前の状態(undisclosure)と、後の状態(disclosure)を表示してくれる。
{
"undisclosure": {
"_sd": [
"7uysFqYgSX5gZahrhttOY1yqxh6AAjCn6xMfIgNVvAI",
"8zzBViW9r_0lV4RRXcMkaUBECedwna0OcIMtIyQWjsA",
"BxJ_SHRgICXqr8CFwLEfT6oEUGp4bKkFPUk1yvpnzCM",
"Gk8n41pvxZwm9cofkS_MA-wkf66mvOUEEWAVPaZzybk",
"Q5gpcsXoHuYkxwTwaUvxaDwTZbdq99njn4c4B_eqZSQ",
"VJukScFAaqQXZQXKGN51RkqeQ7qAP5NsRQ2JGxjF0Pg",
"VnLXcj_4N5WWrUa5zx-dOXxN1uEHmIQq0Su2BcYwdvM",
"kTraLj7llcxxlkyWkPuvNmPd75eMCNSN3GOzZh6xxbE",
"mhohHg4JnjC6_n-nUr0o1duYOolHHansDcMOVbr2q6o",
"oQ4oR1eDfsRPGARxzq6ClkFRo79vD2EbzqSXUCTL8To",
"qGz_iFSJ-RJloz4gAP6j9nlzJBaoHwBlKB6-PRVPWwQ",
"vxUOjRgNhxVMGoov0uErN3mKquLJ7jBGTmtxDn1OSFw"
],
"vct": "https://credentials.example.com/identity_credential",
"_sd_alg": "sha-256",
"iss": "https://trial.authlete.net",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "Q4XG0Gg9UC85DqoYsvs_dJNqAU2m1ybTHP9mFaT3-oQ",
"x": "1_TDHhuo-t5Y6_hpHLQ3sqfWBwxde48itrC6YjjIUmg",
"y": "FJqZhlYQ3qVfr_84MwYiQ6bAl1-oUsJB9tqLMtvdXE0"
}
},
"iat": 1705819268
},
"disclosure": {
"birthdate": "1991-11-06",
"family_name": "Silverstone",
"sub": "1004",
"given_name": "Inga",
"vct": "https://credentials.example.com/identity_credential",
"iss": "https://trial.authlete.net",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "Q4XG0Gg9UC85DqoYsvs_dJNqAU2m1ybTHP9mFaT3-oQ",
"x": "1_TDHhuo-t5Y6_hpHLQ3sqfWBwxde48itrC6YjjIUmg",
"y": "FJqZhlYQ3qVfr_84MwYiQ6bAl1-oUsJB9tqLMtvdXE0"
}
},
"iat": 1705819268
}
}
体験してみる
id walletサーバを立ち上げ、以下のようにAuthlete社のデモサイトにアクセスし、sd-jwt形式のcredentialをもらうまでを体験する
#本当はsd-jwt対応のid walletアプリを見つけて試したかったが、2024/1時点では見つからず..。
1. id walletサーバを起ちあげ
index.mjs, package.js, index.html, qr-scanner.min.js, qr-scanner-worker.min.jsを以下のフォルダ構成のように配置する.qr-scanner関係のjsは、https://github.com/nimiq/qr-scannerからdownloadする。
├── index.mjs
├── package.json
└── views/
├── index.html
├── qr-scanner.min.js
├── qr-scanner-worker.min.js
index.mjsのコードを見る
import { CredentialOfferClient, MetadataClient,} from '@sphereon/oid4vci-client';
import { SDJwt } from '@eengineer1/sd-jwt-ts-node'
import fetch from 'node-fetch';
import crypto from 'crypto';
import * as jose from 'jose';
import express from 'express';
import http from 'http';
import path from 'path';
import { fileURLToPath } from 'url';
import bodyParser from 'body-parser';
//************
let hostname = 'localhost';
const port=8800;
let keys={};
let aud={};
const ALG="ES256"; // ES256, ES256K, EdDSA
if (process.argv.length > 2){ hostname = process.argv[2];}
//*************
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use('/', express.static(__dirname + '/views/'));
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' }));
const httpServer = http.createServer(app);
const server = httpServer.listen(port, () => {
console.log('\n web page start: listening on port %s...', server.address().port);
});
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"){
console.log("init")
let cred = await getCredential(req.body.qr)
res.send(cred);
}
})
async function getCredential(_initiationURI, _alg){
const initiationRequestWithUrl = await CredentialOfferClient.fromURI(_initiationURI);
const metadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(initiationRequestWithUrl);
const clientId = '218232426';
const tokenReq = "client_id="+clientId
+ "&grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code&pre-authorized_code="
+ initiationRequestWithUrl.preAuthorizedCode;
const accessTokenResponse = await fetch(metadata.token_endpoint, {
method: 'post',
body: tokenReq,
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
const accessToken = await accessTokenResponse.json();
if (typeof accessToken.access_token !=="undefined"){
console.log(accessToken);
const proofJwt = await generateKeyProof (ALG,clientId, accessToken.c_nonce, metadata.issuer)
const credentialReq = {
"format": "vc+sd-jwt",
"vct": "https://credentials.example.com/identity_credential",
"proof": {
"proof_type": "jwt",
"jwt":proofJwt
}
};
console.log(credentialReq)
const credentialResponse = await fetch(metadata.credential_endpoint, {
method: 'post',
body: JSON.stringify(credentialReq),
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer '+accessToken.access_token}
});
const credential = await credentialResponse.json();
console.log(credential);
// verify // todo
// parse
const decode = await sdJwtParse(credential.credential, -1);
return {result:true, detail:credential.credential, decode:decode};
}else{
return {result:false, detail:"", decode:""};
}
}
//////////////////////////////////////////////////
async function generateKeyProof (_alg, _clientid, _nonce, _aud){
const { privateKey, publicKey } = await jose.generateKeyPair(_alg);
let ecjwk = await jose.exportJWK(publicKey);
let ecjwkpri = await jose.exportJWK(privateKey);
let _key={"publicJwk":ecjwk,"privateJwk":ecjwkpri};
let args = {
"payload":{
"iat":Math.floor(new Date().getTime() / 1000),
"iss":_clientid,
"nonce": _nonce,
"aud":_aud,
},
"header":{
"typ":"openid4vci-proof+jwt",
"alg":_alg,
"jwk":_key.publicJwk
}
}
const privateKeyObj = await jose.importJWK(ecjwkpri, _alg);
const jwt = await new jose.SignJWT({ ...args.payload })
.setProtectedHeader({...args.header})
.sign(privateKeyObj);
console.log("-> " + jwt)
return jwt;
}
async function sdJwtParse(_sdPayload, _index){
let sdPayload;
const tmp = _sdPayload.split("~");
sdPayload=tmp[0];
for (let i=1; i< tmp.length; i++){
if (i != _index){
sdPayload += "~"+tmp[i];
}
}
const sdJwt = SDJwt.parse(sdPayload)
const parse = {undisclosure:sdJwt.sdPayload.undisclosedPayload, disclosure:sdJwt.fullPayload}
console.log(JSON.stringify(parse, null,1))
return parse
}
package.jsonのコードを見る
{
"dependencies": {
"@eengineer1/sd-jwt-ts-node": "^1.0.1",
"@sphereon/oid4vci-client": "^0.8.2-next.34",
"express": "^4.18.2",
"jose": "^5.2.0",
"uint8arrays": "^5.0.1"
}
}
index.htmlのコードを見る
<!DOCTYPE html>
<html lang='en'>
<head><meta charset='utf-8'></head>
<body>
<div align=center>
<div id="title">[ID Wallet]</div><br>
<button id="scanQR">scan QR code</button>, <button id="stop-button">Stop</button>
<p><div id=msg></div>
<p><div id=qr></div>
<div id="video-container">
<video id="qr-video"></video>
</div>
</div>
<b>Detected QR code: </b>
<span id="cam-qr-result">None</span>
<div id="credresult"></div>
<br>
<hr></body>
<script type="module">
import QrScanner from "./qr-scanner.min.js";
let credentials =[]
const video = document.getElementById('qr-video');
const videoContainer = document.getElementById('video-container');
const camQrResult = document.getElementById('cam-qr-result');
const credResult = document.getElementById('credresult');
function setResult(label, result) {
console.log(result.data);
scanner.stop();
label.textContent = result.data;
label.style.color = 'teal';
_init(result.data)
}
const scanner = new QrScanner(video, result => setResult(camQrResult, result), {
onDecodeError: error => {
camQrResult.textContent = error;
camQrResult.style.color = 'inherit';
},
highlightScanRegion: true,
highlightCodeOutline: true,
});
document.getElementById('scanQR').addEventListener('click', () => {
scanner.start();
});
function _init(_qrcode) {
let setting={
qr:_qrcode
}
sendData("./.issuer_init", JSON.stringify(setting), 'application/json',
function(data){
data = JSON.parse(data)
if (data.result){
credentials.push({type:data.type, detail:data.detail, decode:data.decode})
credResult.innerHTML = "<pre>"+JSON.stringify(credentials[credentials.length-1].decode.undisclosure, null,"\t")+"</pre>---><pre>"+JSON.stringify(credentials[credentials.length-1].decode.disclosure, null,"\t")+"</pre>";
}else{
credResult.innerHTML = "<br>--> failed<br>";
}
}
);
}
// for debugging
window.scanner = scanner;
document.getElementById('stop-button').addEventListener('click', () => {
scanner.stop();
});
</script>
<script type="text/javascript">
window.addEventListener('load',function() {
});
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);
}
</script>
</html>
2. 関連モジュールをインストールする
> npm install
3. id wallet サーバに割り当てるドメイン名を取得
> ngrok http 8800
Forwarding https://fe86-153-214-37-81.ngrok-free.app -> http://localhost:8800
4. id wallet サーバを起動する
> nodejs index.mjs
web page start: listening on port 8800...
5. スマホ内のブラウザでhttps://fe86-153-214-37-81.ngrok-free.app/
にアクセス
- スマホで表示されたwebページ内にある「scan QR code」ボタンを押す。
- PCなどで、authelete社のデモサイトにアクセスし、QRコードを表示
- スマホでそのQRコードをscanする