<!doctype html>
|
|
<head>
|
|
<link rel="stylesheet" href="css/style.v2.css">
|
|
<link rel="shortcut icon" href="https://www.pathcheck.org/hubfs/Favicon.png">
|
|
<title>Vaccine Distribution Certificates Generator</title>
|
|
</head>
|
|
<body>
|
|
<div class="center">
|
|
<div class="full-div">
|
|
<h3>OpenCerta Certificate Example Schema using PCF's spec (<a href="https://github.com/Path-Check/paper-cred/">Spec</a>)</h3>
|
|
<div class="quarter">
|
|
<h4>VC Overhead</h4>
|
|
<table>
|
|
<tr><td>Context</td><td><input id="qr-head-context" type="text" placeholder="https://www.w3.org/2018/credentials/v1"/></td></tr>
|
|
<tr><td>IssuanceDate</td><td><input id="qr-head-issuance-date" type="text" placeholder="Last Name ..."/></td></tr>
|
|
<tr><td>Type</td><td><input id="qr-head-type" type="text" placeholder="VerifiableCredential, Immunization"/></td></tr>
|
|
</table>
|
|
|
|
<h4>FHIR Overhead</h4>
|
|
<table>
|
|
<tr><td>Type</td><td><input id="qr-fhir-type" type="text" placeholder="fhir:Immunization"/></td></tr>
|
|
<tr><td>Context</td><td><input id="qr-fhir-context" type="text" placeholder="https://opencerta.org/certificate/v1/"></td></tr>
|
|
<tr><td>Status</td><td><input id="qr-fhir-status" type="text" placeholder="Completed"/></td></tr>
|
|
<tr><td>NotGiven</td><td><input id="qr-fhir-not-given" type="text" placeholder="false"/></td></tr>
|
|
<tr><td>VaxCode</td><td><input id="qr-fhir-vax-code" type="text" placeholder="AZD1222"/></td></tr>
|
|
<tr><td>VaxCodeText</td><td><input id="qr-fhir-vax-code-text" type="text" placeholder="SARS-CoV-2"/></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Patient</h4>
|
|
<table>
|
|
<tr><td>FirstName</td><td><input id="qr-pat-firstname" type="text" placeholder="First Name ..."/></td></tr>
|
|
<tr><td>LastName</td><td><input id="qr-pat-lastname" type="text" placeholder="Last Name ..."/></td></tr>
|
|
<tr><td>Gender</td><td><input id="qr-pat-gender" type="text" placeholder="Gender ..."/></td></tr>
|
|
</table>
|
|
|
|
<h4>Location</h4>
|
|
<table>
|
|
<tr><td>City</td><td><input id="qr-location-city" type="text" placeholder="Somerville"/></td></tr>
|
|
<tr><td>State</td><td><input id="qr-location-state" type="text" placeholder="MA"/></td></tr>
|
|
<tr><td>Country</td><td><input id="qr-location-country" type="text" placeholder="USA"/></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Immunization</h4>
|
|
<table>
|
|
<tr><td>Date</td><td><input id="qr-immunization-date" type="text" placeholder=""/></td></tr>
|
|
<tr><td>Manuf</td><td><input id="qr-immunization-manuf" type="text" placeholder="AstraZeneca, ..."/></td></tr>
|
|
<tr><td>Product</td><td><input id="qr-immunization-product" type="text" placeholder="SARS-CoV-2"/></td></tr>
|
|
<tr><td>Lot#</td><td><input id="qr-immunization-lot" type="text" placeholder="012L20A, ..."/></td></tr>
|
|
<tr><td>Expiration</td><td><input id="qr-immunization-expiration" type="text" placeholder=""/></td></tr>
|
|
<tr><td>PrimarySource</td><td><input id="qr-immunization-primary-source" type="text" placeholder="true"/></td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="quarter">
|
|
<h4>Practitioner</h4>
|
|
<table>
|
|
<tr><td>FirstName</td><td><input id="qr-practitioner-firstname" type="text" placeholder="First Name ..."/></td></tr>
|
|
<tr><td>LastName</td><td><input id="qr-practitioner-lastname" type="text" placeholder="Last Name ..."/></td></tr>
|
|
<tr><td>Prefix</td><td><input id="qr-practitioner-prefix" type="text" placeholder="Dr."/></td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="quarter">
|
|
<h4>Credentials</h4>
|
|
<label for="privkey">Private Key <small>(openssl ecparam -name secp256k1 -genkey -out private.key)</small></label><br/>
|
|
<textarea id="privkey" rows="10" cols="30">-----BEGIN EC PARAMETERS-----
|
|
BgUrgQQACg==
|
|
-----END EC PARAMETERS-----
|
|
-----BEGIN EC PRIVATE KEY-----
|
|
MHQCAQEEIPWKbSezZMY1gCpvN42yaVv76Lo47FvSsVZpQl0a5lWRoAcGBSuBBAAK
|
|
oUQDQgAE6DeIun4EgMBLUmbtjQw7DilMJ82YIvOR2jz/IK0R/F7/zXY1z+gqvFXf
|
|
DcJqR5clbAYlO9lHmvb4lsPLZHjugQ==
|
|
-----END EC PRIVATE KEY-----</textarea>
|
|
<br><br>
|
|
<label for="pubkey">DNS TXT Record of the Public Key <small>(openssl ec -in private.key -pubout -out public.key)</small></label><br/>
|
|
<textarea id="qr-link" rows="1" cols="30">keys.pathcheck.org</textarea>
|
|
<br><br>
|
|
<label for="privkey">QR Code Format</label><br/>
|
|
<pre>cred:<span class='protocol'>type:version</span>:<span class='signature'>Signature</span>:<span class='pub-key'>PubKey</span>:<span class='message'>Payload</span></pre>
|
|
</div>
|
|
</div>
|
|
<div class="full-div"></div>
|
|
<div class="four-quarter">
|
|
<br><br>
|
|
<div style="margin: 0 auto; width:240px; height:50px;">
|
|
<button class="qr-btn" onclick="generateQRCodes()">Generate Certificates</button>
|
|
</div>
|
|
<br><br>
|
|
</div>
|
|
<div class="full-div">
|
|
<div class="two-quarter">
|
|
<h4>PCF's Format</h4>
|
|
<canvas id="qr-certa-code"></canvas><br/>
|
|
<pre id="qr-certa-result"></pre>
|
|
<pre id="qr-certa-verified"></pre>
|
|
<pre id="qr-certa-bytes"></pre>
|
|
</div>
|
|
<div class="two-quarter">
|
|
<h4>Original OpenCerta Format</h4>
|
|
<img id="qr-certa-orig-code" src='img/open-certa-original.png' style="display: none;"/>
|
|
<pre id="qr-certa-orig-result" style="display: none;">
|
|
{
|
|
"@context": "https://www.w3.org/2018/credentials/v1",
|
|
"type": ["VerifiableCredential", "Immunization"],
|
|
"issuanceDate": "2020-07-10T19:23:24Z",
|
|
"credentialSubject": {
|
|
"type": "fhir:Immunization",
|
|
"@context": "https://opencerta.org/certificate/v1",
|
|
"role": "fhir:treeRoot",
|
|
"Immunization.status": {
|
|
"value": "completed"
|
|
},
|
|
"Immunization.notGiven": {
|
|
"boolean": false
|
|
},
|
|
"Immunization.vaccineCode": {
|
|
"CodeableConcept.coding": [{
|
|
"index": 0,
|
|
"Coding.display": {
|
|
"value": "AZD1222"
|
|
}
|
|
}],
|
|
"CodeableConcept.text": {
|
|
"value": "SARS-CoV-2"
|
|
}
|
|
},
|
|
"Immunization.patient": {
|
|
"Patient.name": [{
|
|
"index": 0,
|
|
"HumanName.family": {
|
|
"value": "Donald"
|
|
},
|
|
"HumanName.given": [{
|
|
"index": 0,
|
|
"value": "Daisy"
|
|
}]
|
|
}],
|
|
"Patient.gender": {
|
|
"value": "female"
|
|
}
|
|
},
|
|
"Immunization.date": {
|
|
"date": "2020-07-10"
|
|
},
|
|
"Immunization.primarySource": {
|
|
"boolean": true
|
|
},
|
|
"Immunization.manufacturer": {
|
|
"Organization.name": {
|
|
"value": "AstraZeneca; The University of Oxford; IQVIA"
|
|
}
|
|
},
|
|
"Immunization.lotNumber": {
|
|
"value": "AAJN11K"
|
|
},
|
|
"Immunization.expirationDate": {
|
|
"date": "2021-07-09"
|
|
},
|
|
"Immunization.location": {
|
|
"Location.address": {
|
|
"Address.city": {
|
|
"value": "Houston"
|
|
},
|
|
"Address.state": {
|
|
"value": "TX"
|
|
},
|
|
"Address.country": {
|
|
"value": "US"
|
|
}
|
|
}
|
|
},
|
|
"Immunization.practitioner": [{
|
|
"index": 0,
|
|
"Practitioner.name": [{
|
|
"index": 0,
|
|
"HumanName.family": {
|
|
"value": "Careful"
|
|
},
|
|
"HumanName.given": [{
|
|
"index": 0,
|
|
"value": "Adam"
|
|
}],
|
|
"HumanName.prefix": [{
|
|
"index": 0,
|
|
"value": "Dr"
|
|
}]
|
|
}]
|
|
}]
|
|
},
|
|
"proof": {...
|
|
}
|
|
}
|
|
|
|
</pre>
|
|
<pre id="qr-certa-verified"></pre>
|
|
<pre id="qr-certa-bytes"></pre>
|
|
</div>
|
|
|
|
<div class="quarter">
|
|
<label for="verify">Verify a QR Code</small></label><br/>
|
|
<textarea id="qr-verify" rows="10" cols="30" placeholder="cred:type:version:signature:pubkey:payload"></textarea>
|
|
<br><br>
|
|
<button class="qr-btn" onclick="verifyQRCode()">Verify</button>
|
|
<br><br>
|
|
<pre id="qr-verify-result"></pre>
|
|
<pre id="qr-verify-verified"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/qrcode.min.js"></script>
|
|
|
|
<script src="js/elliptic.min.js"></script>
|
|
<script src="js/sha256.js"></script>
|
|
<script src="js/asn1.min.js"></script>
|
|
<script src="js/base32.min.js"></script>
|
|
|
|
<script src="js/pcf.js"></script>
|
|
|
|
<script>
|
|
function e(elem) { return document.getElementById(elem); }
|
|
|
|
function getValueArray(elemArray) {
|
|
const fields = elemArray.map(function(elemId) {
|
|
return e(elemId).value;
|
|
})
|
|
return fields;
|
|
}
|
|
|
|
function signAndDisplayQR(elemPref, _type, _version, priKeyPEM, pubKeyId, payloadValueArray) {
|
|
const uri = PCF.sign(_type, _version, priKeyPEM, pubKeyId, payloadValueArray);
|
|
const [schema, type, version, signature, pubKeyLink, payload] = PCF.parseURI(uri);
|
|
|
|
const params = { margin:0, width:275, errorCorrectionLevel: 'L', color: {dark: '#3654DD' }};
|
|
|
|
// Builds QR Element
|
|
QRCode.toCanvas(e(elemPref+'-code'), uri, { margin:0, width:520, errorCorrectionLevel: 'M', color: {dark: '#3654DD' }}, function (error) {
|
|
})
|
|
|
|
// Updates screen elements.
|
|
e(elemPref+"-result").innerHTML= schema+":<span class='protocol'>"+type+":"+version+"</span>:" +
|
|
"<span class='signature'>" + signature + "</span>" + ":" +
|
|
"<span class='pub-key'>" + pubKeyLink + "</span>" + ":" +
|
|
"<span class='message'>" + payload + "</span><br>";
|
|
// "Signature DER: <span class='signature'>" + signatureDER + "</span><br><br>"+
|
|
// "Payload SHA256 Hash: <span class='message'>" + payloadSHA256 + "</span><br><br>";
|
|
e(elemPref+"-verified").innerHTML = "Signature: Not Verified";
|
|
|
|
//e(elemPref+"-bytes").innerHTML = "QR Size: "+ Math.round((uri.length / 2) * 11/8) + " bytes";
|
|
|
|
let qr = QRCode.create(uri, params);
|
|
|
|
let qrQ = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'Q', color: {dark: '#3654DD' }});
|
|
let qrH = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'H', color: {dark: '#3654DD' }});
|
|
let qrM = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'M', color: {dark: '#3654DD' }});
|
|
let qrL = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'L', color: {dark: '#3654DD' }});
|
|
|
|
e(elemPref+"-bytes").innerHTML = "URI in A/N (5.5bit/char): "+ Math.round(uri.length * 5.5/8) + " bytes<br>";
|
|
|
|
e(elemPref+"-bytes").innerHTML += "<br>QR Size Analysis: ";
|
|
e(elemPref+"-bytes").innerHTML += "<br>- ECC L 7% " + describe(qrL);
|
|
e(elemPref+"-bytes").innerHTML += "<br>- ECC M 15% " + describe(qrM);
|
|
e(elemPref+"-bytes").innerHTML += "<br>- ECC Q 25% " + describe(qrQ);
|
|
e(elemPref+"-bytes").innerHTML += "<br>- ECC H 30% " + describe(qrH);
|
|
|
|
e(elemPref+"-bytes").innerHTML += "<br><br>QR built with " + qr.segments.length + " segments";
|
|
for (i=0; i<qr.segments.length; i++) {
|
|
e(elemPref+"-bytes").innerHTML += "<br>- " + i + ": " + qr.segments[i].mode.id + " " + qr.segments[i].data;
|
|
}
|
|
|
|
// Verifies URI is valid
|
|
e(elemPref + "-verified").innerHTML = PCF.debugDownloadVerify(pubKeyLink, payload, signature);
|
|
}
|
|
|
|
function describe(qr) {
|
|
return qr.modules.size + "x" + qr.modules.size + " bits " + Math.round((qr.modules.size*qr.modules.size)/8) + " bytes ";
|
|
}
|
|
|
|
function generateQRCodes() {
|
|
// Where to Download the public key
|
|
const pubKeyLink = e("qr-link").value.trim().replace("http://","");
|
|
|
|
// PEM code of the private key
|
|
const priKeyPEM = e('privkey').value;
|
|
|
|
// Coupon QR
|
|
const certaArray = [
|
|
"qr-pat-firstname", "qr-pat-lastname", "qr-pat-gender",
|
|
"qr-immunization-date", "qr-immunization-manuf", "qr-immunization-product", "qr-immunization-lot", "qr-immunization-expiration", "qr-immunization-primary-source",
|
|
"qr-location-city", "qr-location-state", "qr-location-country",
|
|
"qr-practitioner-firstname", "qr-practitioner-lastname", "qr-practitioner-prefix",
|
|
"qr-head-context", "qr-head-issuance-date", "qr-head-type",
|
|
"qr-fhir-type", "qr-fhir-context", "qr-fhir-not-given", "qr-fhir-status", "qr-fhir-vax-code", "qr-fhir-vax-code-text"
|
|
];
|
|
|
|
signAndDisplayQR("qr-certa", "certa", "1", priKeyPEM, pubKeyLink, getValueArray(certaArray));
|
|
|
|
e("qr-certa-orig-code").style.display='block';
|
|
e("qr-certa-orig-result").style.display='block';
|
|
}
|
|
|
|
function verifyQRCode() {
|
|
e("qr-verify-result").innerHTML = PCF.debugParseURI(e("qr-verify").value);
|
|
e('qr-verify-verified').innerHTML = PCF.debugVerify(e("qr-verify").value);
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
// Defaults
|
|
e("qr-immunization-date").value = new Date().toJSON().slice(0, 10).replaceAll("-","");
|
|
|
|
function loadDemo() {
|
|
e("qr-head-context").value = "https://www.w3.org/2018/credentials/v1";
|
|
e("qr-head-issuance-date").value = new Date().toJSON().slice(0, 10).replaceAll("-","");
|
|
e("qr-head-type").value = "VerifiableCredential, Immunization";
|
|
|
|
e("qr-fhir-type").value = "fhir:Immunization";
|
|
e("qr-fhir-context").value = "https://opencerta.org/certificate/v1/";
|
|
e("qr-fhir-not-given").value = "false";
|
|
e("qr-fhir-status").value = "Completed";
|
|
e("qr-fhir-vax-code").value = "AZD1222";
|
|
e("qr-fhir-vax-code-text").value = "SARS-CoV-2";
|
|
|
|
e("qr-pat-firstname").value = "Daisy";
|
|
e("qr-pat-lastname").value = "Donald";
|
|
e("qr-pat-gender").value = "Female";
|
|
|
|
e("qr-immunization-manuf").value = "AstraZeneca; The University of Oxford; IQVIA";
|
|
e("qr-immunization-product").value = "COVID19";
|
|
e("qr-immunization-lot").value = "AAJN11K";
|
|
e("qr-immunization-expiration").value = "20210709";
|
|
e("qr-immunization-primary-source").value = "true";
|
|
|
|
e("qr-location-city").value = "Houston";
|
|
e("qr-location-state").value = "TX";
|
|
e("qr-location-country").value = "USA";
|
|
|
|
e("qr-practitioner-firstname").value = "Adam";
|
|
e("qr-practitioner-lastname").value = "Careful";
|
|
e("qr-practitioner-prefix").value = "Dr.";
|
|
}
|
|
|
|
loadDemo();
|
|
</script>
|
|
|
|
<script>
|
|
async function preloadKey() {
|
|
PCF.getKeyId(e("qr-link").value);
|
|
}
|
|
window.onload = function() {
|
|
preloadKey();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|