UNPKG

certificate-fhir-convertor

Version:

Vaccination context for verifiable certificate

223 lines (188 loc) 8.65 kB
const { v4: uuidv4 } = require('uuid'); const R = require('ramda'); const Mustache = require("mustache"); const fs = require('fs'); const config = require('./configs/config'); const base64url = require('base64url'); const QRCode = require('qrcode'); const JSZip = require("jszip"); const rs = require("jsrsasign"); const JWS = rs.jws.JWS; const TEMPLATES_FOLDER = __dirname +'/configs/templates/'; const r4TemplateFile = 'fhir-r4.template'; const questionnairResponseTemplateFile = 'questionnaireResponse.template'; const smartHealthTemplateFile = 'smartHealthCard.template'; const CERTIFICATE_FILE = "certificate.json"; function render(template, data) { return JSON.parse(Mustache.render(template, data)) } function dobOfRecipient(certificate) { const dob = R.pathOr('', ['credentialSubject', 'dob'], certificate); const age = R.pathOr('', ['credentialSubject', 'age'], certificate); if (dob && new Date(dob).getFullYear() > 1900) return dob; // administrative dob if (age && age > 0) return (new Date().getFullYear() - age) + "-01-01"; return ""; } function getDataFromCertificate(certificate, meta) { const dateString = new Date().toJSON(); const patientId = uuidv4(); const organisationId = uuidv4(); const qrId = uuidv4(); const bundleId = uuidv4(); const compositionId = uuidv4(); const immunizationId = uuidv4(); const questionnaireResponseId = uuidv4(); const practitionerName = R.pathOr('', ['evidence', 0, 'verifier', 'name'], certificate); const vaccineName = R.pathOr('', ['evidence', 0, 'vaccine'], certificate); const icd11Code = R.pathOr('', ['evidence', 0, 'icd11Code'], certificate); if (!icd11Code && !config.VACCINE_MAPPINGS.filter(a => vaccineName.toLowerCase().includes(a.vaccineName)).map(a => a.icd11Code)[0]) { throw new Error("unsupported vaccine name "+ vaccineName) } const vaccineCode = !icd11Code ? config.VACCINE_MAPPINGS.filter(a => vaccineName.toLowerCase().includes(a.vaccineName)).map(a => a.icd11Code)[0]: icd11Code; const facilityName = R.pathOr('', ['evidence', 0, 'facility', 'name'], certificate); const facilityCity = R.pathOr('', ['evidence', 0, 'facility', 'address', 'city'], certificate); const facilityDistrict = R.pathOr('', ['evidence', 0, 'facility', 'address', 'district'], certificate); const facilityCountry = R.pathOr('', ['evidence', 0, 'facility', 'address', 'addressCountry'], certificate); const facilityId = facilityName.split(' ').join('-'); const patientNationality = R.pathOr('', ['credentialSubject', 'nationality'], certificate); const patientGovtId = R.pathOr('', ['credentialSubject', 'id'], certificate); const patientName = R.pathOr('', ['credentialSubject', 'name'], certificate); const refId = R.pathOr('', ['credentialSubject', 'refId'], certificate); const dob = dobOfRecipient(certificate); const patientGender = R.pathOr('', ['credentialSubject', 'gender'], certificate).toLowerCase(); const vaccinationDate = R.pathOr('', ['evidence', 0, 'date'], certificate); const manufacturer = R.pathOr('', ['evidence', 0, 'manufacturer'], certificate); const batchNumber = R.pathOr('', ['evidence', 0, 'batch'], certificate); const effectiveUntilDate = R.pathOr('', ['evidence', 0, 'effectiveUntil'], certificate); const effectiveStartDate = R.pathOr('', ['evidence', 0, 'effectiveStart'], certificate); const dose = parseInt(R.pathOr('', ['evidence', 0, 'dose'], certificate)); const totalDoses = parseInt(R.pathOr('', ['evidence', 0, 'totalDoses'], certificate)); const certificateId = R.pathOr('', ['evidence', 0, 'certificateId'], certificate); const diseaseCode = R.pathOr('', ['diseaseCode'], meta); const publicHealthAuthority = R.pathOr('', ['publicHealthAuthority'], meta); return { dateString, patientId, organisationId, qrId, bundleId, compositionId, immunizationId, practitionerName, vaccineName, vaccineCode, facilityName, facilityCity, facilityDistrict, facilityCountry, facilityId, patientNationality, patientGovtId, patientName, dob, patientGender, vaccinationDate, manufacturer, batchNumber, effectiveUntilDate, effectiveStartDate, dose, totalDoses, diseaseCode, refId, publicHealthAuthority, certificateId, questionnaireResponseId }; } async function certificateToFhirJson(certificate, privateSigningKeyPem, meta) { let data = getDataFromCertificate(certificate, meta); // build QR data and img const QR_template = fs.readFileSync(TEMPLATES_FOLDER+questionnairResponseTemplateFile, 'utf8'); const qrPayload = JSON.stringify(render(QR_template, data)); const qrContent = sign(qrPayload, privateSigningKeyPem); const zip = new JSZip(); zip.file(CERTIFICATE_FILE, qrContent, { compression: "DEFLATE" }); const zippedData = await zip.generateAsync({type: "string", compression: "DEFLATE"}) .then(function (content) { // console.log(content) return content; }); const qrImage = await QRCode.toDataURL(zippedData, {scale: 2}); data = {...data, qrContent, qrImage}; const template = fs.readFileSync(TEMPLATES_FOLDER+r4TemplateFile, 'utf8'); let fhirCert = render(template, data); return signFhirCert(fhirCert, data.vaccinationDate, privateSigningKeyPem) } function certificateToSmartHealthJson(certificate, meta) { let data = getDataFromCertificate(certificate, meta); // build Smart Health Payload const QR_template = fs.readFileSync(TEMPLATES_FOLDER+smartHealthTemplateFile, 'utf8'); return render(QR_template, data) } function signFhirCert(fhirJson, vaccinationDate, privateKeyPem) { const token = sign(JSON.stringify(fhirJson), privateKeyPem); const splittedToken = token.split("."); splittedToken[1] = ""; const detachedPayloadJWS = splittedToken.join("."); const organisationId = fhirJson.entry.filter(r => r.resourceType === "Organization").map(r => r.id)[0]; let signature = { "type": [ { "system": "urn:iso-astm:E1762-95:2013", "code": "1.2.840.10065.1.12.1.5", "display": "Verification Signature" } ], "when": new Date().toJSON(), "who": { "reference": "urn:uuid:"+organisationId }, "targetFormat": "application/fhir+json", "sigFormat": "application/jose", "data": detachedPayloadJWS } fhirJson["signature"] = signature; return fhirJson; } function validateSignedFhirJson(fhirJson, publicSigningKeyPem) { const signature = fhirJson.signature; if (!signature || signature === "") { console.error("signature data is not present in bundle resource"); return false } // removing the signature let payload = fhirJson; delete payload.signature; let splittedJWS = signature.data.split("."); splittedJWS[1] = base64url(JSON.stringify(payload), "utf8"); try { return verify(splittedJWS.join("."), publicSigningKeyPem) } catch(err) { console.error(err); return false } } function validateQRContent(content, publicSigningKeyPem) { try { return verify(content, publicSigningKeyPem) } catch(err) { console.error(err); return false } } function sign(payload, privateKeyPem) { // const keypair = crypto.generateKeyPairSync( // 'ed25519', // { // privateKeyEncoding: { format: 'pem', type: 'pkcs8' }, // publicKeyEncoding: { format: 'pem', type: 'spki' } // } // ) // // console.log(keypair.privateKey) // console.log(keypair.publicKey) // var ecprv = new rs.crypto.ECDSA({'prv': privateKeyPem, 'curve': 'secp256r1'}); // return JWS.sign("ES256", {}, payload, ecprv) const prvKeyObj = rs.KEYUTIL.getKey(privateKeyPem); let alg = ''; if (prvKeyObj instanceof rs.RSAKey) alg = 'RS256'; else if (prvKeyObj instanceof rs.crypto.ECDSA) alg = 'ES256'; else throw "unsupported signature type"; return JWS.sign(alg, {}, payload, prvKeyObj) } function verify(content, publicKeyPem) { const pubKeyObj = rs.KEYUTIL.getKey(publicKeyPem); let acceptField = {}; acceptField.alg = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512']; return rs.jws.JWS.verifyJWT(content, pubKeyObj, acceptField); } module.exports = { certificateToFhirJson, certificateToSmartHealthJson, validateSignedFhirJson, validateQRContent };