<!doctype html>
|
|
<head>
|
|
<link rel="stylesheet" href="css/style.verify.css">
|
|
<link rel="stylesheet" href="css/topnav.css">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="shortcut icon" href="https://www.pathcheck.org/hubfs/Favicon.png">
|
|
<title>Certificate Verifier</title>
|
|
</head>
|
|
<body>
|
|
<div class="topnav">
|
|
<div class="topnavContainer">
|
|
<a href="index.html">Signers</a>
|
|
<a class="active" href="verify.html"><span class="xs-hidden">Universal </span>Verifier</a>
|
|
<a class="xs-hidden" href="debug.html">QR Debugger</a>
|
|
<a class="xs-hidden" href="https://github.com/Path-Check/paper-cred-demo">Source Code</a>
|
|
<a href="https://github.com/Path-Check/paper-cred"><span class="xs-hidden"> QR </span>Specs</a>
|
|
<a href="http://vaccine-docs.pathcheck.org"><span class="xs-hidden">Vaccine </span>Docs</a>
|
|
<a class="xs-hidden" href="http://pathcheck.org">About PathCheck</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="center">
|
|
<div class="full-div">
|
|
<div id="pre-verify-section">
|
|
<h1>Certificate Verifier</h1>
|
|
|
|
<div class="select" tabindex="1" id="cameras"></div>
|
|
<button class="camera-btn" id='camera-btn' onclick="toggleCamera()">Go</button>
|
|
<br><br>
|
|
|
|
<video id="preview" ></video>
|
|
|
|
<h2>Or Paste the QR Code here:</h2>
|
|
<textarea id="qr-verify" rows="5" placeholder="cred:type:version:signature:pubkey:payload"></textarea>
|
|
<br><br>
|
|
|
|
<div class="align-center">
|
|
<button class="qr-btn" onclick="verifyQRCode()">Verify</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="post-verify-section" style="display: none;">
|
|
<div class="auto-margin">
|
|
<div class="card align-center">
|
|
<h2 id="qr-verify-title" style="text-align: center;"></h1>
|
|
<canvas id="qr"></canvas><br/>
|
|
<h2 id="qr-verify-name" style="text-align: center;"></h1>
|
|
<h3 id="qr-verify-verified" style="text-align: center;"></h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/qrcode.min.js"></script>
|
|
|
|
<script src="js/pcf-utils.js"></script>
|
|
<script src="js/ui-utils.js"></script>
|
|
|
|
<script src="js/pcf.sdk.min.js"></script>
|
|
<script src="js/divoc.sdk.min.js"></script>
|
|
<script src="js/eudgc.sdk.min.js"></script>
|
|
|
|
<script type="text/javascript" src="js/instascan.min.js"></script>
|
|
|
|
<script>
|
|
function e(elem) { return document.getElementById(elem); }
|
|
|
|
function qrParams(elem) { return {margin:0, width:Math.min(e(elem).scrollWidth,500)-20, errorCorrectionLevel: 'L', color: {dark: '#3654DD' }}; }
|
|
|
|
var scanner = new Instascan.Scanner({ video: document.getElementById('preview'), mirror: false });
|
|
scanner.addListener('scan', function (content) {
|
|
onScanSuccess(content);
|
|
});
|
|
scanner.addListener('active', function (content) {
|
|
onCameraOn(content);
|
|
})
|
|
scanner.addListener('inactive', function (content) {
|
|
onCameraOff(content);
|
|
})
|
|
|
|
const monthNames = [ " ", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
|
|
|
|
function parseDate(str) {
|
|
console.log(str)
|
|
if(!/^(\d){8}$/.test(str)) return "invalid date";
|
|
var y = str.substr(0,4),
|
|
m = str.substr(4,2),
|
|
d = str.substr(6,2);
|
|
return monthNames[parseInt(m)] + ' ' + d + ', ' + y;
|
|
}
|
|
|
|
// Date in seconds from epoch
|
|
function parseDInt(str) {
|
|
const d = new Date(parseInt(str) * 1000);
|
|
return monthNames[parseInt(d.getMonth()+1)] + ' ' + d.getDate() + ', ' + d.getFullYear();
|
|
}
|
|
|
|
function parseISODate(str) {
|
|
const d = new Date(str);
|
|
return monthNames[parseInt(d.getMonth()+1)] + ' ' + d.getDate() + ', ' + d.getFullYear();
|
|
}
|
|
|
|
function short(hash) {
|
|
return hash.substr(0,10) + ".." + hash.substr(hash.length-10,10)
|
|
}
|
|
|
|
function verifyQRCode(data) {
|
|
if (!data)
|
|
data = e("qr-verify").value;
|
|
|
|
console.log(data);
|
|
|
|
// Update the page's address without causing a reload
|
|
window.location.hash = '#processed'
|
|
certView();
|
|
|
|
if (data === "" || data == null) {
|
|
displayCertificateNotFound(data);
|
|
} else if (typeof(data) === 'string' && data.startsWith("CRED:")) {
|
|
verifyDisplayCRED(data);
|
|
} else if (data.startsWith("PK")) {
|
|
unZipVerifyDisplayDIVOC(data);
|
|
} else if (typeof(data) === 'string' && data.startsWith("{")) {
|
|
if (data.includes("did:india")) {
|
|
verifyDisplayDIVOC(json);
|
|
} else {
|
|
verifyDisplayIBM(json);
|
|
}
|
|
} else if (typeof(data) === 'string' && data.startsWith("HC1:")) {
|
|
verifyDisplayEU(data);
|
|
} else {
|
|
displayCertificateUnsupported(data);
|
|
}
|
|
}
|
|
|
|
function displayCertificateUnsupported(data) {
|
|
e('qr-verify-title').innerHTML = '';
|
|
e('qr-verify-name').innerHTML = '';
|
|
let canvas = e('qr');
|
|
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
|
e('qr-verify-verified').innerHTML = "Certificate not found.";
|
|
}
|
|
|
|
function displayCertificateUnsupported(data) {
|
|
e('qr-verify-title').innerHTML = '';
|
|
e('qr-verify-name').innerHTML = '';
|
|
let canvas = e('qr');
|
|
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
|
e('qr-verify-verified').innerHTML = "Unsupported Certificate.";
|
|
}
|
|
|
|
async function verifyDisplayCRED(data) {
|
|
let uri = data.substring(data.indexOf("CRED:"));
|
|
|
|
const [schema, type, version, signatureBase32NoPad, pubKeyLink, payloadStr] = await PCF.unpack(uri);
|
|
const payload = await PCF.unpackAndVerify(uri);
|
|
const fields = await PCF.mapHeaders(payload, type, version);
|
|
|
|
QRCode.toCanvas(e('qr'), uri, qrParams('qr'), function (error) {
|
|
UIUtils.drawVerifiedSymbol('qr',payload);
|
|
});
|
|
|
|
if (payload) {
|
|
e('qr-verify-verified').innerHTML = 'Signed by ' + pubKeyLink + (fields['date'] || fields['issuanceDate'] ? ' on ' + parseDate(fields['date'] ? fields['date'] : fields['issuanceDate']) : '');
|
|
} else {
|
|
e('qr-verify-verified').innerHTML = "Credential Invalid or Not Supported";
|
|
}
|
|
|
|
if (type == 'BADGE') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Record"
|
|
e('qr-verify-name').innerHTML = (fields['name'] ? fields['name'] : short(fields['passkey'])) + "<br>";
|
|
e('qr-verify-name').innerHTML += fields['dob'] ? parseDate(fields['dob']) : "" + "<br>";
|
|
} else if (type == 'COUPON') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Coupon"
|
|
e('qr-verify-name').innerHTML = "Coupon " + fields['number'] + "/" + fields['total'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "<small>Phase " + fields['phase'] + " in " + fields['city'] + "</small><br>";
|
|
} else if (type == 'STATUS') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Pass"
|
|
e('qr-verify-name').innerHTML = "Status: " + (fields['status'] === '2' ? "Vaccinated" : fields['status'] === '1' ? "One Dose" : "Not Vaccinated") + "<br />";
|
|
e('qr-verify-name').innerHTML += (fields['initials'] ? "Initials: " + fields['initials'] : "Key: " + short(fields['passkey'])) + "<br>";
|
|
} else if (type == 'PASSKEY') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 PassKey"
|
|
e('qr-verify-name').innerHTML = "Name: " + fields['name'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + parseDate(fields['dob']);
|
|
} else if (type == 'LIBERTY') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Pass"
|
|
e('qr-verify-name').innerHTML = fields['credentialSubject.subject.name.given'] + " " + fields['credentialSubject.subject.name.family'] + "<br>";
|
|
e('qr-verify-name').innerHTML += parseDate(fields['credentialSubject.subject.birthDate']);
|
|
} else if (type == 'COWIN') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Record"
|
|
e('qr-verify-name').innerHTML = fields['credentialSubject.name'] + "<br>";
|
|
e('qr-verify-name').innerHTML += fields['credentialSubject.age'] + "yrs old";
|
|
} else if (type == 'EU.DGC.VAX') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Certificate"
|
|
e('qr-verify-name').innerHTML = fields['nam.gn'] + " " + fields['nam.fn'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + parseDInt(fields['dob']);
|
|
} else if (type == 'EU.DGC.TEST') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Test Certificate"
|
|
e('qr-verify-name').innerHTML = fields['nam.gn'] + " " + fields['nam.fn'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + parseDInt(fields['dob']);
|
|
} else if (type == 'EU.DGC.RECV') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Recovery Certificate"
|
|
e('qr-verify-name').innerHTML = fields['nam.gn'] + " " + fields['nam.fn'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + parseDInt(fields['dob']);
|
|
} else if (type == 'EU.DGC.RECV') {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Recovery Certificate"
|
|
e('qr-verify-name').innerHTML = fields['nam.gn'] + " " + fields['nam.fn'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + parseDInt(fields['dob']);
|
|
} else if (type == 'VIAL') {
|
|
e('qr-verify-title').innerHTML = "Vaccine Vial"
|
|
e('qr-verify-name').innerHTML = fields['manuf'] + " " + fields['product'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "Lot: #" + fields['lot'];
|
|
} else if (type == 'US.MA.ID') {
|
|
e('qr-verify-title').innerHTML = "Mass ID (demo)"
|
|
e('qr-verify-name').innerHTML = fields['dad'] + " " + fields['dct'] + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + parseDate(fields['dbc']);
|
|
} else if (type == 'BANKNOTE') {
|
|
e('qr-verify-title').innerHTML = "Bank Note (demo)"
|
|
e('qr-verify-name').innerHTML = fields['denomination'] + " " + fields['curency-name'] + "<br>";
|
|
e('qr-verify-name').innerHTML += fields['country'];
|
|
} else {
|
|
e('qr-verify-title').innerHTML = "Unsupported Credential"
|
|
e('qr-verify-name').innerHTML = "Credential Type " + type + " undefined";
|
|
}
|
|
}
|
|
|
|
function getEUPayload(cwt) {
|
|
if (cwt instanceof Map) {
|
|
return cwt.get(-260).get(1);
|
|
}
|
|
return cwt;
|
|
}
|
|
|
|
function verifyDisplayEU(data) {
|
|
EUDGC.debug(data).then(obj => {
|
|
let result = null;
|
|
|
|
QRCode.toCanvas(e('qr'), data, qrParams('qr'), function (error) {
|
|
UIUtils.drawVerifiedSymbol('qr',result);
|
|
});
|
|
|
|
if (result == null) {
|
|
e('qr-verify-verified').innerHTML = "Unable to Verify";
|
|
} else if (result) {
|
|
e('qr-verify-verified').innerHTML = 'Signed by ' + json.evidence[0].facility.name + ' on ' + parseISODate(json.issuanceDate);
|
|
} else {
|
|
e('qr-verify-verified').innerHTML = "Credential Invalid";
|
|
}
|
|
|
|
console.log(obj);
|
|
let certificate = obj.value[2].get(-260).get(1);
|
|
console.log(certificate);
|
|
|
|
if (certificate.v) {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Certificate"
|
|
} else if (certificate.t) {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Test Certificate"
|
|
} else if (certificate.r) {
|
|
e('qr-verify-title').innerHTML = "COVID-19 Recovery Certificate"
|
|
}
|
|
|
|
e('qr-verify-name').innerHTML = certificate.nam.gn + " " + certificate.nam.fn + "<br>";
|
|
e('qr-verify-name').innerHTML += "DOB: " + certificate.dob ;
|
|
});
|
|
}
|
|
|
|
function unZipVerifyDisplayDIVOC(data) {
|
|
DIVOC.unpack(data).then(function (contents) {
|
|
console.log("json", contents);
|
|
verifyDisplayDIVOC(contents);
|
|
});
|
|
}
|
|
|
|
function verifyDisplayDIVOC(json) {
|
|
DIVOC.verify(json).then(result => {
|
|
DIVOC.pack(json).then(function (bin) {
|
|
QRCode.toCanvas(e('qr'), bin, qrParams('qr'), function (error) {
|
|
UIUtils.drawVerifiedSymbol('qr',result);
|
|
});
|
|
});
|
|
|
|
if (result == null) {
|
|
e('qr-verify-verified').innerHTML = "Unable to Verify";
|
|
} else if (result) {
|
|
e('qr-verify-verified').innerHTML = 'Signed by ' + json.evidence[0].facility.name + ' on ' + parseISODate(json.issuanceDate);
|
|
} else {
|
|
e('qr-verify-verified').innerHTML = "Credential Invalid";
|
|
}
|
|
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Record"
|
|
e('qr-verify-name').innerHTML = json.credentialSubject.name + "<br>";
|
|
e('qr-verify-name').innerHTML += json.credentialSubject.age + "yrs old";
|
|
});
|
|
}
|
|
|
|
function verifyDisplayIBM(json) {
|
|
//IBM.verify(json).then(result => {
|
|
let result = null;
|
|
|
|
//IBM.pack(json).then(function (bin) {
|
|
QRCode.toCanvas(e('qr'), JSON.stringify(json), qrParams('qr'), function (error) {
|
|
UIUtils.drawVerifiedSymbol('qr',result);
|
|
});
|
|
//});
|
|
|
|
if (result == null) {
|
|
e('qr-verify-verified').innerHTML = "Unable to Verify";
|
|
} else if (result) {
|
|
e('qr-verify-verified').innerHTML = 'Signed by ' + json.evidence[0].facility.name + ' on ' + parseISODate(json.issuanceDate);
|
|
} else {
|
|
e('qr-verify-verified').innerHTML = "Credential Invalid";
|
|
}
|
|
|
|
e('qr-verify-title').innerHTML = "COVID-19 Vaccine Pass"
|
|
e('qr-verify-name').innerHTML = json.credentialSubject.subject.name.given + " " + json.credentialSubject.subject.name.family + "<br>";
|
|
e('qr-verify-name').innerHTML += "DoB: " + json.credentialSubject.subject.birthDate;
|
|
//});
|
|
}
|
|
|
|
function onScanSuccess(qrMessage) {
|
|
stopCamera();
|
|
|
|
if (qrMessage !== "" && qrMessage != null) {
|
|
e("qr-verify").value = qrMessage;
|
|
verifyQRCode(qrMessage);
|
|
}
|
|
}
|
|
|
|
function onScanFailure(error) {
|
|
console.warn(`QR error = ${error}`);
|
|
}
|
|
|
|
function getQueryVariable(variable) {
|
|
var query = window.location.search.substring(1);
|
|
var vars = query.split('&');
|
|
for (var i = 0; i < vars.length; i++) {
|
|
var pair = vars[i].split('=');
|
|
if (pair[0] == variable) {
|
|
return pair[1];
|
|
}
|
|
}
|
|
console.log('Query variable %s not found', variable);
|
|
}
|
|
|
|
function toggleCamera() {
|
|
if (e('camera-btn').innerHTML === "Go") {
|
|
startCamera();
|
|
} else {
|
|
stopCamera();
|
|
}
|
|
}
|
|
|
|
function onCameraOn() {
|
|
e('camera-btn').innerHTML = "Stop";
|
|
}
|
|
function onCameraOff() {
|
|
e('camera-btn').innerHTML = "Go";
|
|
}
|
|
|
|
function startCamera() {
|
|
Instascan.Camera.getCameras().then(function (cameras) {
|
|
if (cameras.length > 0) {
|
|
scanner.start(cameras[document.querySelector('input[name="camera"]:checked').id.substr(3)]);
|
|
} else {
|
|
console.error('No cameras found.');
|
|
}
|
|
}).catch(function (e) {
|
|
console.error(e);
|
|
});
|
|
}
|
|
|
|
function stopCamera() {
|
|
scanner.stop();
|
|
e('preview').pause();
|
|
e('preview').removeAttribute('src'); // empty source
|
|
e('preview').load();
|
|
}
|
|
|
|
function homeView() {
|
|
e("pre-verify-section").style.display = '';
|
|
e("post-verify-section").style.display = 'none';
|
|
}
|
|
|
|
function certView() {
|
|
e("pre-verify-section").style.display = 'none';
|
|
e("post-verify-section").style.display = '';
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
// Loading URI from qr parameter.
|
|
const queryString = window.location.search;
|
|
|
|
const qr = getQueryVariable("qr")
|
|
|
|
if (qr !== "" && qr != null) {
|
|
console.log(qr);
|
|
e("qr-verify").value = qr;
|
|
verifyQRCode(qr);
|
|
}
|
|
|
|
Instascan.Camera.getCameras().then(function (cameras) {
|
|
let selectedCamIndex = 0;
|
|
cameras.forEach(function(camera, index) {
|
|
if (camera.name) {
|
|
if (camera.name.toLowerCase().indexOf('back') != -1) {
|
|
selectedCamIndex = index;
|
|
}
|
|
if (camera.name.toLowerCase().indexOf('usb') != -1) {
|
|
selectedCamIndex = index;
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log("Camera Index", selectedCamIndex);
|
|
|
|
cameras.forEach(function(camera, index) {
|
|
let newInput = document.createElement("input");
|
|
newInput.setAttribute('class',"selectopt");
|
|
newInput.setAttribute('name',"camera");
|
|
newInput.setAttribute('type',"radio");
|
|
newInput.setAttribute('id',"opt"+index);
|
|
|
|
console.log(camera);
|
|
|
|
if (index == selectedCamIndex)
|
|
newInput.setAttribute('checked',true);
|
|
|
|
e('cameras').appendChild(newInput);
|
|
|
|
newInput = document.createElement("label");
|
|
newInput.setAttribute('for',"opt"+index);
|
|
newInput.setAttribute('class',"option");
|
|
newInput.innerHTML = camera.name;
|
|
|
|
e('cameras').appendChild(newInput);
|
|
});
|
|
});
|
|
|
|
window.onpopstate = function() {
|
|
switch(location.hash) {
|
|
case '#processed':
|
|
certView();
|
|
break
|
|
default:
|
|
homeView();
|
|
}
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|