1
0
mirror of https://github.com/covidpass-org/covidpass.git synced 2025-02-23 15:07:44 +01:00

verifyJWS working now... picked up latest issuer code

This commit is contained in:
Billy Lo 2021-09-29 15:35:02 -04:00
parent db5052aa9a
commit 52d27089cc
4 changed files with 159 additions and 87 deletions

View File

@ -1,6 +1,5 @@
// adapted from https://github.com/fproulx/shc-covid19-decoder/blob/main/src/shc.js
const jose = require("node-jose");
const jsQR = require("jsqr");
const zlib = require("zlib");
import {Receipt} from "./payload";
@ -13,54 +12,6 @@ export function getQRFromImage(imageData) {
);
}
export function getScannedJWS(shcString) {
return shcString
.match(/^shc:\/(.+)$/)[1]
.match(/(..?)/g)
.map((num) => String.fromCharCode(parseInt(num, 10) + 45))
.join("");
}
//TODO: switch to https://github.com/smart-on-fhir/health-cards-dev-tools at some point
export function verifyJWS(jws) {
return jose.JWK.asKey({
kid: "some-kid",
alg: "ES256",
kty: "EC",
crv: "P-256",
use: "sig",
x: "XSxuwW_VI_s6lAw6LAlL8N7REGzQd_zXeIVDHP_j_Do",
y: "88-aI4WAEl4YmUpew40a9vq_w5OcFvsuaKMxJRLRLL0",
}).then(function (key) {
const { verify } = jose.JWS.createVerify(key);
console.log("jws", jws);
return verify(jws);
});
}
export function decodeJWS(jws) : Promise<object[]> {
const verifiedPayload = jws.split(".")[1];
const decodedPayload = Buffer.from(verifiedPayload, "base64");
return new Promise((resolve, reject) => {
zlib.inflateRaw(decodedPayload, function (err, decompressedResult) {
let scannedResult;
if (typeof err === "object" && err) {
console.log("Unable to decompress");
reject();
} else {
console.log(decompressedResult);
scannedResult = decompressedResult.toString("utf8");
const entries =
JSON.parse(scannedResult).vc.credentialSubject.fhirBundle.entry;
resolve(entries);
}
});
});
}
// vaccine codes based on Alex Dunae's findings
// https://gist.github.com/alexdunae/49cc0ea95001da3360ad6896fa5677ec
// http://mchp-appserv.cpe.umanitoba.ca/viewConcept.php?printer=Y&conceptID=1514

66
src/issuers.js Normal file
View File

@ -0,0 +1,66 @@
const issuers = [
{
id: "ca.qc",
iss: "https://covid19.quebec.ca/PreuveVaccinaleApi/issuer",
keys: [
{ kid: "qFdl0tDZK9JAWP6g9_cAv57c3KWxMKwvxCrRVSzcxvM",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "XSxuwW_VI_s6lAw6LAlL8N7REGzQd_zXeIVDHP_j_Do",
y: "88-aI4WAEl4YmUpew40a9vq_w5OcFvsuaKMxJRLRLL0" },
]
},
{
id: "us.ca",
iss: "https://myvaccinerecord.cdph.ca.gov/creds",
keys: [
{ kid: "7JvktUpf1_9NPwdM-70FJT3YdyTiSe2IvmVxxgDSRb0",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "3dQz5ZlbazChP3U7bdqShfF0fvSXLXD9WMa1kqqH6i4",
y: "FV4AsWjc7ZmfhSiHsw2gjnDMKNLwNqi2jMLmJpiKWtE" },
]
},
{
id: "us.ny",
iss: "https://ekeys.ny.gov/epass/doh/dvc/2021",
keys: [
{ kid: "9ENs36Gsu-GmkWIyIH9XCozU9BFhLeaXvwrT3B97Wok",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "--M0AedrNg31sHZGAs6qg7WU9LwnDCMWmd6KjiKfrZU",
y: "rSf2dKerJFW3-oUNcvyrI2x39hV2EbazORZvh44ukjg" },
]
},
{
id: "us.la",
iss: "https://healthcardcert.lawallet.com",
keys: [
{ kid: "UOvXbgzZj4zL-lt1uJVS_98NHQrQz48FTdqQyNEdaNE",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "n1PxhSk7DQj8ZBK3VIfwhlcN__QG357gbiTfZYt1gn8",
y: "ZDGv5JYe4mCm75HCsHG8UkIyffr1wcZMwJjH8v5cGCc" },
]
},
{
id: "ca.yt",
iss: "https://pvc.service.yukon.ca/issuer",
keys: [
{ kid: "UnHGY-iyCIr__dzyqcxUiApMwU9lfeXnzT2i5Eo7TvE",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "wCeT9rdLYTpOK52OK0-oRbwDxbljJdNiDuxPsPt_1go",
y: "IgFPi1OrHtJWJGwPMvlueeHmULUKEpScgpQtoHNjX-Q" },
]
},
{
id: "ca.bc",
iss: "https://smarthealthcard.phsa.ca/v1/issuer",
keys: [
{ kid: "XCqxdhhS7SWlPqihaUXovM_FjU65WeoBFGc_ppent0Q",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "xscSbZemoTx1qFzFo-j9VSnvAXdv9K-3DchzJvNnwrY",
y: "jA5uS5bz8R2nxf_TU-0ZmXq6CKWZhAG1Y4icAx8a9CA" },
]
},
];
module.exports = {
issuers,
};

View File

@ -4,6 +4,7 @@ import jsQR, {QRCode} from "jsqr";
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
import * as Sentry from '@sentry/react';
import * as Decode from './decode';
import {getScannedJWS, verifyJWS, decodeJWS} from "./shc";
import { PNG } from 'pngjs/browser';
import { PDFPageProxy, TextContent, TextItem } from "pdfjs-dist/types/display/api";
@ -89,51 +90,29 @@ async function processBC(pdfPage: PDFPageProxy) {
const imageData = await getImageDataFromPdf(pdfPage);
const code : QRCode = await Decode.getQRFromImage(imageData);
let rawData = code.data;
let jws = Decode.getScannedJWS(rawData);
// const verified = Decode.verifyJWS(jws);
const verified = true;
const jws = getScannedJWS(rawData);
let decoded = await decodeJWS(jws);
console.log(JSON.stringify(decoded, null, 2));
const verified = verifyJWS(jws, decoded.iss);
if (verified) {
let decoded = await Decode.decodeJWS(jws);
console.log(decoded);
let receipts = Decode.decodedStringToReceipt(decoded);
let receipts = Decode.decodedStringToReceipt(decoded.vc.credentialSubject.fhirBundle.entry);
console.log(receipts);
return Promise.resolve({receipts: receipts, rawData: rawData});
} else {
return Promise.reject('QR code not signed by BC or QC');
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
}
}
async function processBCPNG(fileBuffer : ArrayBuffer): Promise<any> {
return new Promise((resolve, reject) => {
new PNG({ filterType: 4 }).parse(fileBuffer, async function (error, data) {
const scannedQR = jsQR(new Uint8ClampedArray(data.data.buffer), data.width, data.height)
if (scannedQR) {
//console.log(scannedQR.data);
let jws = Decode.getScannedJWS(scannedQR.data);
// const verified = Decode.verifyJWS(jws);
const verified = true;
if (verified) {
let decoded = await Decode.decodeJWS(jws);
//console.log(decoded);
let receipts = Decode.decodedStringToReceipt(decoded);
//console.log(receipts);
resolve({receipts: receipts, rawData: scannedQR.data});
} else {
reject('QR code not signed by BC or QC');
}
} else {
throw new Error('Invalid QR code')
}
resolve(data);
});
})
}
async function processON(signedPdfBuffer : ArrayBuffer, content: TextContent): Promise<any> {
// check for certs first
@ -148,9 +127,7 @@ async function processON(signedPdfBuffer : ArrayBuffer, content: TextContent): P
const issuedToOntarioHealth = (result.issuedTo.commonName == 'covid19signer.ontariohealth.ca');
console.log(`PDF is signed by ${result.issuedBy.organizationName}, issued to ${result.issuedTo.commonName}`);
const bypass = window.location.href.includes('grassroots2');
if ((isClientCertificate && issuedByEntrust && issuedToOntarioHealth) || bypass) {
if ((isClientCertificate && issuedByEntrust && issuedToOntarioHealth)) {
console.log('getting receipt details inside PDF');

78
src/shc.js Normal file
View File

@ -0,0 +1,78 @@
const jose = require("node-jose");
const jsQR = require("jsqr");
const zlib = require("zlib");
const { issuers } = require("./issuers");
function getQRFromImage(imageData) {
return jsQR(
new Uint8ClampedArray(imageData.data.buffer),
imageData.width,
imageData.height
);
}
function getScannedJWS(shcString) {
try {
return shcString
.match(/^shc:\/(.+)$/)[1]
.match(/(..?)/g)
.map((num) => String.fromCharCode(parseInt(num, 10) + 45))
.join("");
} catch (e) {
error = new Error("parsing shc string failed");
error.cause = e;
throw error;
}
}
function verifyJWS(jws, iss) {
const issuer = issuers.find(el => el.iss === iss);
if (!issuer) {
error = new Error("Unknown issuer " + iss);
error.customMessage = true;
return Promise.reject(error);
}
return jose.JWK.asKeyStore({ keys: issuer.keys }).then(function (keyStore) {
const { verify } = jose.JWS.createVerify(keyStore);
console.log("jws", jws);
return verify(jws);
});
}
function decodeJWS(jws) {
try {
const payload = jws.split(".")[1];
return decodeJWSPayload(Buffer.from(payload, "base64"));
} catch (e) {
error = new Error("decoding payload failed");
error.cause = e;
throw error;
}
}
function decodeJWSPayload(decodedPayload) {
return new Promise((resolve, reject) => {
zlib.inflateRaw(decodedPayload, function (err, decompressedResult) {
if (typeof err === "object" && err) {
console.log("Unable to decompress");
reject(err);
} else {
try {
console.log(decompressedResult);
scannedResult = decompressedResult.toString("utf8");
resolve(JSON.parse(scannedResult));
} catch (e) {
reject(e);
}
}
});
});
}
module.exports = {
getQRFromImage,
getScannedJWS,
verifyJWS,
decodeJWS,
decodeJWSPayload,
};