Verifying QRs

The official GreenPass apps are usually of two kinds:

  • wallet-like (can store certificates)
  • verifier-like (can NOT persist certificates)

I only guess that this decision was made to ban official forces who use verifier apps to store personal data of verified people. On the other side, there is no such protection for this in wallet apps (so everybody can scan and save QR code of his / her friend) it's still the responsibility of the authorities to ask for ID card, etc.

Verify Example#

BluePass-19 verifies the scanned QRs in accordance with official (country level) apps (for further specification, see motivation page).

Verify Flow#

Here is an explanation of how QR codes are verified. Any thrown errors are mapped to bussiness / UI errors in accordance to this object on next page.

Verify Pseudocode#

  • Strip HC:1 prefix : "HC:1" prefix is removed from scanned from scanned data.
  • Decode by using base45
  • Decompress by Zlib
  • Decompress by CBOR - returns COSE header, body and signature
  • Take KID (Public Key Identifier) from header
  • Decode COSE body using CBOR (second time) - return GreenCertificate / Greenpass JSON
  • Validate GreenPass by JSON Schema
  • Validate IssueDate and ExpiryDate in GreenCertificate header
  • Validate signatures using KID from previous steps (fetch public key by KID, and do crypto verification)
  • Validate GreenPass by Business rules in JsonLogic
  • Validation finished without error - GreenPass is valid

JS Code#

If you prefer code, take a look below:

export async function parseQR(
rawData,
validateSignaturesOnScanning = true,
validateWithBusinessRules = true,
validateDates = true,
) {
if (!rawData.startsWith('HC1:')) {
throw new Error(ERRORS.NOT_SUPPORTED_QR);
}
let coseData;
try {
// 1. remove 'HC1:' from the start
let strippedPrefix = rawData.substring(4);
// 2. base45 decode
let bytesArray = decodeBase45(strippedPrefix);
// 3. apply zlib
let decompressed = decompressZlib(bytesArray);
console.log('Decompressed ', decompressed);
// 4. apply CBOR first time
coseData = decodeCBOR(decompressed);
console.log('coseData ', coseData);
} catch (error) {
console.error(error);
throw new Error(ERRORS.ENVELOPE_PARSING_FAILED);
}
// destructure CBOR data
const [protectedHeader, unprotectedHeader, coseContent, coseSignature] =
coseData;
console.log('Cose Data', {
protectedHeader,
unprotectedHeader,
coseContent,
coseSignature,
});
// get key identifier - KID
const kid = getKeyIdFromHeaders(protectedHeader, unprotectedHeader);
if (!kid) {
throw new Error(ERRORS.KID_MISSING_IN_HEADER);
}
console.warn({KID: kid});
// 5. apply CBOR second time to get inner payload
let greenCertificate = null;
try {
greenCertificate = decodeCBOR(coseContent);
} catch (error) {
console.log(error);
throw new Error(ERRORS.ENVELOPE_PARSING_FAILED);
}
console.log('Green certificate', JSON.stringify(greenCertificate));
if (greenCertificate == null) {
throw new Error(ERRORS.JSON_SCHEMA_VALIDATION_FAILED);
}
if (
!greenCertificate ||
!greenCertificate[GREEN_CERT_CONTENT_ENCODING.HCERT] ||
!greenCertificate[GREEN_CERT_CONTENT_ENCODING.HCERT][
GREEN_CERT_CONTENT_ENCODING.H_CERT_PAYLOAD
]
) {
throw new Error(ERRORS.JSON_SCHEMA_VALIDATION_FAILED);
}
let schemasIsOkResult = schemaValidator(
greenCertificate[GREEN_CERT_CONTENT_ENCODING.HCERT][
GREEN_CERT_CONTENT_ENCODING.H_CERT_PAYLOAD
],
);
console.log('schema is ok', schemasIsOkResult);
if (!schemasIsOkResult.valid) {
console.log('Schema not ok', schemasIsOkResult.errors);
throw new Error(ERRORS.JSON_SCHEMA_VALIDATION_FAILED);
}
if (validateDates) {
let datesAreOk = hasValidDates(
greenCertificate[GREEN_CERT_CONTENT_ENCODING.EXPIRATION],
greenCertificate[GREEN_CERT_CONTENT_ENCODING.ISSUED_AT],
);
console.log('Date are ok', datesAreOk);
if (!datesAreOk) {
throw new Error(ERRORS.ISSUE_OR_EXPIRE_DATES_NOT_OK);
}
}
if (validateSignaturesOnScanning) {
const certs = await RealmInstance.getAllLocalCryptoCertificateItemsForKid(
kid,
);
console.warn('Found certs for kid ', kid, certs);
let validatedAgainstLocalCerts = await certsCryptoValidator(
protectedHeader,
coseContent,
coseSignature,
certs,
);
if (validatedAgainstLocalCerts === false) {
console.log('Some cert validation failed or no certs are present');
throw new Error(ERRORS.VALIDATED_AGAINST_LOCAL_CERT_FAILED);
}
}
if (validateWithBusinessRules) {
const validationClock = nowFormatted();
let greenPassContent =
greenCertificate[GREEN_CERT_CONTENT_ENCODING.HCERT][
GREEN_CERT_CONTENT_ENCODING.H_CERT_PAYLOAD
];
console.warn(greenPassContent, validationClock);
const {validationResult, errors} = await validateBusinessRules(
greenPassContent,
validationClock,
);
if (!validationResult) {
console.log('Some business rules validation failed', errors);
throw new Error(ERRORS.BUSINESS_RULES_VALIDATION_FAILED);
}
}
console.warn('CERT OK');
return Promise.resolve({
greenCertificate:
greenCertificate[GREEN_CERT_CONTENT_ENCODING.HCERT][
GREEN_CERT_CONTENT_ENCODING.H_CERT_PAYLOAD
],
issueDate: greenCertificate[GREEN_CERT_CONTENT_ENCODING.ISSUED_AT],
expiryDate: greenCertificate[GREEN_CERT_CONTENT_ENCODING.EXPIRATION],
rawQRdata: rawData,
id: new BSON.ObjectID(), // equivalent to uuid,
});
}

If you are interested in libraries (dependencies), that were used in React Native, see Installation section.

Browsing Stored Certificates#

If user scanned valid QR certificate, he will have the opportunity to persist it for later use. All stored certificates could be found at the home screen.

Opening Scanned & Stored Certificate#

If authorities would require user to present his certificate, user can open stored certificate where he will see original QR code with all details that are embedded in that certificate.