UNPKG

@digital-blueprint/greenlight-app

Version:

[GitHub Repository](https://github.com/digital-blueprint/greenlight-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/greenlight-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/greenlight-app/) | [Greenlight Bundle](ht

310 lines (285 loc) 8.74 kB
import certlogic from 'certlogic-js'; import {withDate} from './utils'; export class ValueSets { constructor() { /** @type {Date} */ this.validFrom = null; /** @type {Date} */ this.validUntil = null; this.valueSets = []; } /** * Converts an array of value sets to something certlogic can work with * * @returns {Array} */ forLogic() { let logicInput = {}; for (const set of this.valueSets) { logicInput[set.valueSetId] = Object.keys(set.valueSetValues); } return logicInput; } } /** * Decodes the Austrian version of the value sets * * @param {object} hcert * @param {object} trustData * @param {string} trustAnchor * @param {Date} date * @returns {ValueSets} */ export async function decodeValueSets(hcert, trustData, trustAnchor, date) { let decoded = withDate(date, () => { return hcert.SignedDataDownloader.loadValueSets( trustAnchor, trustData['valuesets'], trustData['valuesetssig'] ); }); let result = []; for (const entry of decoded.second.valueSets) { result.push(JSON.parse(entry.valueSet)); } let vs = new ValueSets(); vs.validFrom = new Date(decoded.first.validFrom); vs.validUntil = new Date(decoded.first.validUntil); vs.valueSets = result; return vs; } export class BusinessRules { constructor() { this.rules = []; } /** * Remove all rules matching the partial rule object. * If the passed object is empty, none are removed. * * @param {object} ruleFilter */ removeAll(ruleFilter) { let entries = Object.entries(ruleFilter); if (entries.length === 0) return; this.rules = this.rules.filter((rule) => { for (let [key, value] in entries) { if (rule[key] !== value) return true; } return false; }); } /** * Add a new rule * * @param {object} rule */ add(rule) { this.rules.push(rule); } /** * Filters based on country and region * * @param {string} country * @param {string} region * @returns {BusinessRules} */ filter(country, region) { let filtered = []; for (let rule of this.rules) { if (rule.Country == country && rule.Region == region) { filtered.push(rule); } } let br = new BusinessRules(); br.rules = filtered; return br; } } /** * Decode the Austrian version of the business rules * * @param {object} hcert * @param {object} trustData * @param {string} trustAnchor * @param {Date} date * @returns {BusinessRules} */ export async function decodeBusinessRules(hcert, trustData, trustAnchor, date) { let decoded = withDate(date, () => { return hcert.SignedDataDownloader.loadBusinessRules( trustAnchor, trustData['rules'], trustData['rulessig'] ); }); let result = []; for (const entry of decoded.second.rules) { result.push(JSON.parse(entry.rule)); } let br = new BusinessRules(); br.rules = result; return br; } /** * Decode the Austrian version of the business rules from JSON instead of CBOR * * @param {object} hcert * @param {object} trustData * @param {string} trustAnchor * @param {Date} date * @returns {BusinessRules} */ export async function decodeJSONBusinessRules(hcert, trustData, trustAnchor, date) { let dataView = new DataView(trustData['rules.json']); let decoder = new TextDecoder('utf8'); let decoded = JSON.parse(decoder.decode(dataView)); let result = []; for (const entry of decoded.r) { result.push(JSON.parse(entry.r)); } let br = new BusinessRules(); br.rules = result; return br; } /** * Returns a rule description useable for an error message * * @param {object} rule * @returns {object} */ function getRuleErrorDescriptions(rule) { let descriptions = {}; for (let entry of rule.Description) { descriptions[entry.lang] = `[${rule.Identifier}] ${entry.desc}`; } return descriptions; } export class RuleValidationResult { constructor() { this.isValid = false; this.errors = []; } } /** * Validates a HCERT against specific business rules, value sets and the current time * * Will set isValid to false in case the HCERT breaks one or more rules. * * @param {object} cert * @param {BusinessRules} businessRules * @param {ValueSets} valueSets * @param {Date} date The time used as input for the rules * @param {Date} rulesDate The time used to select the active set of rules * @returns {RuleValidationResult} */ export function validateHCertRules(cert, businessRules, valueSets, date, rulesDate) { let logicInput = { payload: cert, external: { valueSets: valueSets.forLogic(), validationClock: date.toISOString(), }, }; let valResult = new RuleValidationResult(); valResult.isValid = true; let errors = []; for (let rule of businessRules.rules) { // In case a rule isn't valid we should just ignore it. This is usually used to update // rules at a specific time, in which case there will be rule X which will stop being // valid at time T and rule Y which will start being valid at time T. if (rulesDate < new Date(rule.ValidFrom) || rulesDate > new Date(rule.ValidTo)) { continue; } let result = certlogic.evaluate(rule.Logic, logicInput); if (result !== true) { errors.push(getRuleErrorDescriptions(rule)); } } if (errors.length) { valResult.errors = errors; valResult.isValid = false; } return valResult; } /** * Returns a Date until the HCERT is valid. fromDate needs to be a Date at which * the HCERT is valid. If it isn't valid then null is returned instead. In case * it never gets invalid null is also returned; * * Since the rules can change this of course is just a guess, but still helpful * to users. * * This assumes that a valid HCERT that as time goes on becomes invalid will * never become valid again. If that's not the case then the returned result is * undefined. * * @param {object} hcert * @param {BusinessRules} businessRules * @param {ValueSets} valueSets * @param {Date} fromDate * @returns {null|Date} */ export function getValidUntil(hcert, businessRules, valueSets, fromDate) { let isValid = (checkTime) => { // We pass as static fromDate so we always use the same set of rules and // so that validity of HCERTs doesn't flip back from invalid to valid in case // rules change. return validateHCertRules(hcert, businessRules, valueSets, checkTime, fromDate).isValid; }; let fromTimestamp = (timestamp) => { return new Date(timestamp); }; let isValidDate = (date) => { return !isNaN(date.getTime()); }; // If not valid at fromDate then there is no end date if (!isValid(fromDate)) { return null; } // Find a date in the future where the cert is no longer valid // Give up once the timestamp overflows let start = fromDate.getTime(); let offset = 3600 * 1000 * 24; let checkTime = fromDate; // Max 1000 years const maxTime = new Date(fromDate.getTime()); maxTime.setFullYear(maxTime.getFullYear() + 1000); // eslint-disable-next-line no-constant-condition while (1) { let timestamp = start + offset; checkTime = fromTimestamp(timestamp); if (!isValidDate(checkTime) || checkTime > maxTime) { // If we overflow we check the largest possible date // and if that also is valid we give up and assume it // can never become invalid if (!isValid(maxTime)) { checkTime = maxTime; break; } return null; } if (!isValid(checkTime)) { break; } offset *= 2; } // Find the latest date at which the cert is still valid let low = fromDate.getTime(); let high = checkTime.getTime(); while (low < high) { let mid = Math.round(low + (high - low) / 2); checkTime = fromTimestamp(mid); if (!isValid(checkTime)) { if (high === mid) { break; } high = mid; } else { if (low === mid) { break; } low = mid; } } return fromTimestamp(low); }