зеркало из
1
0
Форкнуть 0
This commit is contained in:
Larry Joy 2021-03-05 22:18:25 -08:00
Родитель 24ccfc3019
Коммит 32a064bfff
17 изменённых файлов: 411 добавлений и 631 удалений

Просмотреть файл

@ -19,15 +19,5 @@
"@typescript-eslint"
],
"rules": {
"indent": ["error", 4],
"require-jsdoc":"off",
"semi": [
"error",
"always"
],
"spaced-comment": [
"error",
"always"
]
}
}

4
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
{
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true
}

Просмотреть файл

@ -1,220 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { LogLevels } from "./logger";
// eslint-disable-next-line no-var // TODO: ljoy, can we delete this?
// export class OutputTree {
// public child: OutputTree | undefined;
// public infos: InfoItem[] = [];
// public errors: ErrorItem[] = [];
// private _exitCode = 0;
// constructor(public title: string = '') { }
// public get exitCode(): number {
// if (this.child) {
// const childExitCode = this.child.exitCode;
// // the child should not return a different fatal exitCode
// if (this._exitCode && (childExitCode === this._exitCode)) {
// throw new Error("Exit code overwritten. Should only have one fatal error.");
// }
// // set this exit code to the child if it's currently 0
// this._exitCode = this._exitCode || childExitCode;
// }
// return this._exitCode;
// }
// error(errorItemArray: ErrorItem[]): OutputTree;
// error(message: string): OutputTree;
// error(message: string, code: ErrorCode): OutputTree;
// error(message: string, code: ErrorCode, fatal: boolean): OutputTree;
// error(message: string | ErrorItem[], code: ErrorCode = ErrorCode.ERROR, fatal = false): OutputTree {
// if (typeof message === 'string') {
// if (code == null || code === 0) {
// throw new Error("Non-zero error code required.");
// }
// if (fatal && this._exitCode !== 0) {
// throw new Error("Exit code overwritten. Should only have one fatal error.");
// }
// if (fatal) this._exitCode = code;
// this.errors.push(new ErrorItem(message, code, fatal));
// return this;
// }
// for (let i = 0; i < message.length; i++) {
// const err = message[i];
// if (err.fatal && this._exitCode !== 0) {
// throw new Error("Exit code overwritten. Should only have one fatal error.");
// }
// if (err.fatal) this._exitCode = err.code;
// this.errors.push(err);
// }
// return this;
// }
// info(message: string, code: InfoCode = InfoCode.INFO): OutputTree {
// this.infos.push(new InfoItem(message, code));
// return this;
// }
// warn(message: string, code: InfoCode = InfoCode.INFO): OutputTree {
// this.infos.push(new InfoItem(message, code));
// return this;
// }
// // collects errors from all children into a single collection
// flatten(): { title: string, message: string, code: ErrorCode, fatal: boolean }[] {
// let errors = this.errors.map(e => {
// return {
// title: this.title,
// message: e.message,
// code: e.code,
// fatal: e.fatal
// };
// });
// if (this.child) errors = errors.concat(this.child.flatten());
// return errors;
// }
// }
// eslint-disable-next-line no-var
export class OutputTree {
public child: OutputTree | undefined;
public log: LogItem[] = [];
public result: FhirBundle | JWS | JWSPayload | HealthCard | undefined = undefined;
private _exitCode = 0;
constructor(public title: string = '') { }
public get exitCode(): number {
if (this.child) {
const childExitCode = this.child.exitCode;
// the child should not return a different fatal exitCode
if (this._exitCode && (childExitCode === this._exitCode)) {
throw new Error("Exit code overwritten. Should only have one fatal error.");
}
// set this exit code to the child if it's currently 0
this._exitCode = this._exitCode || childExitCode;
}
return this._exitCode;
}
debug(message: string): OutputTree {
this.log.push(new LogItem(message, 0, LogLevels.DEBUG));
return this;
}
info(message: string): OutputTree {
this.log.push(new LogItem(message, 0, LogLevels.INFO));
return this;
}
warn(message: string, code: ErrorCode = ErrorCode.ERROR): OutputTree {
if (code == null || code === 0) {
throw new Error("Non-zero error code required.");
}
this.log.push(new LogItem(message, code, LogLevels.WARNING));
return this;
}
error(message: string, code: ErrorCode = ErrorCode.ERROR): OutputTree {
if (code == null || code === 0) {
throw new Error("Non-zero error code required.");
}
this.log.push(new LogItem(message, code, LogLevels.ERROR));
return this;
}
fatal(message: string, code: ErrorCode = ErrorCode.ERROR): OutputTree {
if (code == null || code === 0) {
throw new Error("Non-zero error code required.");
}
if (this._exitCode !== 0) {
throw new Error("Exit code overwritten. Should only have one fatal error.");
}
this._exitCode = code;
this.log.push(new LogItem(message, code, LogLevels.FATAL));
return this;
}
add(logs: LogItem[]): OutputTree {
for (let i = 0; i < logs.length; i++) {
const item = logs[i];
switch (item.logLevel) {
case LogLevels.DEBUG:
return this.debug(item.message);
case LogLevels.INFO:
return this.info(item.message);
case LogLevels.WARNING:
return this.warn(item.message, item.code);
case LogLevels.ERROR:
return this.error(item.message, item.code);
case LogLevels.FATAL:
return this.fatal(item.message, item.code);
}
}
return this;
}
get(level: LogLevels): LogItem[] {
return this.log.filter(item => {
return item.logLevel === level;
});
}
// collects errors from all children into a single collection
flatten(level: LogLevels = LogLevels.DEBUG): { title: string, message: string, code: ErrorCode, level: LogLevels }[] {
let items = this.log
.filter((item) => {
return item.logLevel >= level;
})
.map(e => {
return {
title: this.title,
message: e.message,
code: e.code,
level: e.logLevel
};
});
if (this.child) items = items.concat(this.child.flatten(level));
return items;
}
}
export class LogItem {
constructor(public message: string, public code: ErrorCode = 0, public logLevel: LogLevels = LogLevels.INFO) { }
}
export enum ErrorCode {
ERROR = 100,
DATA_FILE_NOT_FOUND,
@ -230,25 +16,16 @@ export enum ErrorCode {
JSON_PARSE_ERROR,
CRITICAL_DATA_MISSING,
JWS_TOO_LONG,
INVALID_FILE_EXTENSION
}
export enum InfoCode {
INFO = 0
}
export class ErrorWithCode extends Error {
constructor(message: string, public code: ErrorCode) {
super(message);
}
}
export class ResultWithErrors {
public result: string | undefined = undefined;
public errors: LogItem[] = [];
error(message: string, code: ErrorCode, level = LogLevels.ERROR): ResultWithErrors {
this.errors.push(new LogItem(message, code, level));
return this;
}
INVALID_FILE_EXTENSION,
INVALID_MISSING_KTY = 200,
INVALID_WRONG_KTY,
INVALID_MISSING_ALG,
INVALID_WRONG_ALG,
INVALID_MISSING_USE,
INVALID_WRONG_USE,
INVALID_MISSING_KID,
INVALID_WRONG_KID,
INVALID_SCHEMA,
INVALID_UNKNOWN
}

Просмотреть файл

@ -2,28 +2,32 @@
// Licensed under the MIT license.
import * as utils from './utils';
import {validateSchema} from './schema';
import { OutputTree, ErrorCode } from './error';
import { validateSchema } from './schema';
import { ErrorCode } from './error';
import fhirBundleSchema from '../schema/fhir-bundle-schema.json';
import { Log } from './logger';
import { ValidationResult } from './validate';
export const schema = fhirBundleSchema;
export function validate(fhirBundleText: string): OutputTree {
const output = new OutputTree('FhirBundle');
export function validate(fhirBundleText: string): ValidationResult {
const output = new Log('FhirBundle');
const fhirBundle = utils.parseJson<FhirBundle>(fhirBundleText);
if (fhirBundle === undefined) {
return output
.fatal("Failed to parse FhirBundle data as JSON.", ErrorCode.JSON_PARSE_ERROR);
return {
result: fhirBundle,
log: output.fatal("Failed to parse FhirBundle data as JSON.", ErrorCode.JSON_PARSE_ERROR)
}
}
// returns [] if successful
const schemaResults = validateSchema(fhirBundleSchema, fhirBundle);
output.add(schemaResults);
// failures will be recorded in the log. we can continue processing.
validateSchema(fhirBundleSchema, fhirBundle, output);
// to continue validation, we must have a jws-compact string to validate
@ -32,11 +36,16 @@ export function validate(fhirBundleText: string): OutputTree {
fhirBundle.entry.length === 0
) {
// The schema check above will list the expected properties/type
return output.error("FhirBundle.entry[] required to contine.");
return {
result: fhirBundle,
log: output.fatal("FhirBundle.entry[] required to contine.", ErrorCode.CRITICAL_DATA_MISSING)
}
}
output.info("Fhir Bundle Contents:");
output.info(JSON.stringify(fhirBundle, null, 2));
return output;
}
return { result: fhirBundle, log: output };
}

Просмотреть файл

@ -3,25 +3,31 @@
import * as utils from './utils';
import { validateSchema } from './schema';
import { OutputTree, ErrorCode } from './error';
import { ErrorCode } from './error';
import healthCardSchema from '../schema/smart-health-card-schema.json';
import * as jws from './jws-compact';
import { Log } from './logger';
import { ValidationResult } from './validate';
export const schema = healthCardSchema;
export async function validate(healthCardText: string): Promise<OutputTree> {
const output = new OutputTree('SMART Health Card');
export async function validate(healthCardText: string): Promise<ValidationResult> {
const log = new Log('SMART Health Card');
const healthCard = utils.parseJson<HealthCard>(healthCardText);
if (healthCard == undefined) {
return output
.fatal("Failed to parse HealthCard data as JSON.", ErrorCode.JSON_PARSE_ERROR);
return {
result: healthCard,
log: log.fatal("Failed to parse HealthCard data as JSON.", ErrorCode.JSON_PARSE_ERROR)
}
}
// returns [] if successful
const schemaResults = validateSchema(healthCardSchema, healthCard);
output.add(schemaResults);
// failures will be recorded in the log. we can continue processing.
validateSchema(healthCardSchema, healthCard, log);
// to continue validation, we must have a jws-compact string to validate
const vc = healthCard.verifiableCredential;
@ -32,12 +38,16 @@ export async function validate(healthCardText: string): Promise<OutputTree> {
typeof vc[0] !== 'string'
) {
// The schema check above will list the expected properties/type
return output.fatal(
"HealthCard.verifiableCredential[jws-compact] required to contine.",
ErrorCode.CRITICAL_DATA_MISSING);
return {
result: healthCard,
log: log.fatal("HealthCard.verifiableCredential[jws-compact] required to contine.", ErrorCode.CRITICAL_DATA_MISSING)
}
}
output.child = await jws.validate(vc[0]);
return output;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
log.child = (await jws.validate(vc[0])).log;
return { result: healthCard, log: log };
}

Просмотреть файл

@ -2,7 +2,7 @@
// Licensed under the MIT license.
import { validateSchema } from './schema';
import { OutputTree, ErrorCode } from './error';
import { ErrorCode } from './error';
import jwsCompactSchema from '../schema/jws-schema.json';
import * as jwsPayload from './jws-payload';
import * as keys from './keys';
@ -10,31 +10,38 @@ import pako from 'pako';
import got from 'got/dist/source';
import jose, { JWK } from 'node-jose';
import path from 'path';
import { Log } from './logger';
import { ValidationResult } from './validate';
const MAX_JWS_LENGTH = 1195;
export const schema = jwsCompactSchema;
const MAX_JWS_LENGTH = 1195;
export async function validate(jws: string): Promise<OutputTree> {
export async function validate(jws: JWS): Promise<ValidationResult> {
// the jws string is not JSON. It is base64url.base64url.base64url
const output = new OutputTree('JWS-compact');
const log = new Log('JWS-compact');
if (!/[0-9a-zA-Z_-]+\.[0-9a-zA-Z_-]+\.[0-9a-zA-Z_-]+/g.test(jws.trim())) {
return output
.fatal('Failed to parse JWS-compact data as \'base64url.base64url.base64url\' string.',
ErrorCode.JSON_PARSE_ERROR);
return {
result: undefined,
log: log.fatal('Failed to parse JWS-compact data as \'base64url.base64url.base64url\' string.', ErrorCode.JSON_PARSE_ERROR)
}
}
if (jws.length >= MAX_JWS_LENGTH) {
output.error('JWS, at ' + jws.length.toString() + ' characters, exceeds max character length of ' + MAX_JWS_LENGTH.toString(), ErrorCode.JWS_TOO_LONG);
log.error('JWS, at ' + jws.length.toString() + ' characters, exceeds max character length of ' + MAX_JWS_LENGTH.toString(), ErrorCode.JWS_TOO_LONG);
}
// returns [] if successful
const schemaResults = validateSchema(jwsCompactSchema, jws);
output.add(schemaResults);
// failures will be recorded in the log. we can continue processing.
validateSchema(jwsCompactSchema, jws, log);
// split into header[0], payload[1], key[2]
@ -45,9 +52,10 @@ export async function validate(jws: string): Promise<OutputTree> {
let inflatedPayload;
try {
inflatedPayload = pako.inflateRaw(Buffer.from(rawPayload, 'base64'), { to: 'string' });
log.info('JWS payload inflated');
} catch (err) {
// TODO: we should try non-raw inflate, or try to parse JSON directly (if they forgot to deflate) and continue, to report the exact error
output.error(
log.error(
["Error inflating JWS payload. Did you use raw DEFLATE compression?",
(err as string)].join('\n'),
ErrorCode.INFLATION_ERROR);
@ -55,41 +63,44 @@ export async function validate(jws: string): Promise<OutputTree> {
// try to validate the payload (even if infation failed)
output.child = jwsPayload.validate(inflatedPayload || rawPayload);
const payload = output.child.result as JWSPayload;
const payloadResult = jwsPayload.validate(inflatedPayload || rawPayload);
const payload = payloadResult.result as JWSPayload;
log.child = payloadResult.log;
// if we did not get a payload back, it failed to be parsed and we cannot extract the key url
// so we can stop.
// the jws-payload child will contain the parse errors.
// The payload validation may have a Fatal error if
if (payload == null) {
return output;
return { result: payload, log : log };
}
// Extract the key url
if (!payload.iss) {
// continue, since we might have the key we need in the global keystore
output.error("Can't find 'iss' entry in JWS payload",
log.error("Can't find 'iss' entry in JWS payload",
ErrorCode.SCHEMA_ERROR);
}
// download the keys into the keystore. if it fails, continue an try to use whatever is in the keystore.
await downloadKey(path.join(payload.iss, '/.well-known/jwks.json'), output);
await downloadKey(path.join(payload.iss, '/.well-known/jwks.json'), log);
if(await verifyJws(jws, output)) {
output.info("JWS signature verified");
if(await verifyJws(jws, log)) {
log.info("JWS signature verified");
}
return output;
// TODO: the result should probably be the expanded (non-compact) JWS object.
return { result: jws, log : log };
}
async function downloadKey(keyPath: string, log: OutputTree): Promise<JWK.Key[] | undefined> {
async function downloadKey(keyPath: string, log: Log): Promise<JWK.Key[] | undefined> {
log.info("Retrieving issuer key from " + keyPath);
@ -110,7 +121,8 @@ async function downloadKey(keyPath: string, log: OutputTree): Promise<JWK.Key[]
}
async function verifyJws(jws: string, log: OutputTree): Promise<boolean> {
async function verifyJws(jws: string, log: Log): Promise<boolean> {
const verifier: jose.JWS.Verifier = jose.JWS.createVerify(keys.store);
@ -124,4 +136,4 @@ async function verifyJws(jws: string, log: OutputTree): Promise<boolean> {
}
return false;
}
}

Просмотреть файл

@ -3,31 +3,32 @@
import * as utils from './utils';
import { validateSchema } from './schema';
import { OutputTree, ErrorCode } from './error';
import { ErrorCode } from './error';
import jwsPayloadSchema from '../schema/smart-health-card-vc-schema.json';
import * as fhirBundle from './fhirBundle';
import { Log } from './logger';
import { ValidationResult } from './validate';
export const schema = jwsPayloadSchema;
export function validate(jwsPayloadText: string): OutputTree {
const output = new OutputTree('JWS.payload');
export function validate(jwsPayloadText: string): ValidationResult {
const log = new Log('JWS.payload');
const jwsPayload = utils.parseJson<JWSPayload>(jwsPayloadText);
if (jwsPayload === undefined) {
return output
.fatal("Failed to parse JWS.payload data as JSON.", ErrorCode.JSON_PARSE_ERROR);
return {
result: jwsPayload,
log: log.fatal("Failed to parse JWS.payload data as JSON.", ErrorCode.JSON_PARSE_ERROR)
}
}
// this will get passed back to the jws-compact validation so it can
// pull out the url for the key
output.result = jwsPayload;
// returns [] if successful
const schemaResults = validateSchema(jwsPayloadSchema, jwsPayload);
output.add(schemaResults);
// failures will be recorded in the log. we can continue processing.
validateSchema(jwsPayloadSchema, jwsPayload, log);
// to continue validation, we must have a jws-compact string to validate
@ -37,13 +38,16 @@ export function validate(jwsPayloadText: string): OutputTree {
!jwsPayload.vc.credentialSubject.fhirBundle
) {
// The schema check above will list the expected properties/type
return output.fatal("JWS.payload.vc.credentialSubject.fhirBundle{} required to contine.",
ErrorCode.CRITICAL_DATA_MISSING);
return {
result: jwsPayload,
log: log.fatal("JWS.payload.vc.credentialSubject.fhirBundle{} required to contine.", ErrorCode.CRITICAL_DATA_MISSING)
}
}
const fhirBundleText = JSON.stringify(jwsPayload.vc.credentialSubject.fhirBundle);
output.child = fhirBundle.validate(fhirBundleText);
log.child = (fhirBundle.validate(fhirBundleText)).log;
return output;
}
return { result: jwsPayload, log: log };
}

Просмотреть файл

@ -1,14 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import path from 'path';
import fs from 'fs';
import { ErrorCode } from './error';
export enum ValidationErrors {
UNKNOWN,
// TODO: all errors
export class LogItem {
constructor(public message: string, public code: ErrorCode = 0, public logLevel: LogLevels = LogLevels.INFO) { }
}
export enum LogLevels {
// Print out everything
DEBUG = 0,
@ -22,132 +22,94 @@ export enum LogLevels {
FATAL
}
interface LogEntry {
level: LogLevels,
validationError: ValidationErrors,
message: string,
details?: unknown
}
/**
* Logs application messages. Each message has a level; the message will be
* printed if its level is higher than the logger's verbosity.
*/
class Logger {
// eslint-disable-next-line no-var
export class Log {
public child: Log | undefined;
public log: LogItem[] = [];
private _exitCode = 0;
/**
* Constructs a logger.
* @param level mininum verbosity level to log
* @param outFilePath path to output the logs to if specified, console otherwise
*/
constructor(level: LogLevels, outFilePath?: string) {
this._verbosity = level;
constructor(public title: string = '') { }
if (outFilePath) {
// create new console that writes to file
outFilePath = path.normalize(outFilePath);
const ws = fs.createWriteStream(outFilePath);
this._console = new console.Console(ws, ws);
this._usesStdOut = false;
public get exitCode(): number {
if (this.child) {
const childExitCode = this.child.exitCode;
// the child should not return a different fatal exitCode
if (this._exitCode && (childExitCode === this._exitCode)) {
throw new Error("Exit code overwritten. Should only have one fatal error.");
}
// set this exit code to the child if it's currently 0
this._exitCode = this._exitCode || childExitCode;
}
return this._exitCode;
}
_console: Console = console;
_usesStdOut = true;
_verbosity: LogLevels;
_logEntries: LogEntry[] = [];
_prefix = false;
get verbosity(): LogLevels {
return this._verbosity;
debug(message: string): Log {
this.log.push(new LogItem(message, 0, LogLevels.DEBUG));
return this;
}
set verbosity(level: LogLevels) {
if (!Number.isInteger(level) || level < LogLevels.DEBUG || level > LogLevels.FATAL) {
throw new Error("Invalid verbosity level");
}
this._verbosity = level;
info(message: string): Log {
this.log.push(new LogItem(message, 0, LogLevels.INFO));
return this;
}
set prefix(on: boolean) {
this._prefix = on;
warn(message: string, code: ErrorCode = ErrorCode.ERROR): Log {
if (code == null || code === 0) {
throw new Error("Non-zero error code required.");
}
this.log.push(new LogItem(message, code, LogLevels.WARNING));
return this;
}
color(s: string, color: string): string {
if (this._usesStdOut) {
// color the output, only on STDOUT
// if (color == 'red') {
// s = '\x1b[31m' + s + '\x1b[0m'; // red - message - reset
// } else if (color == 'yellow') {
// s = '\x1b[33m' + s + '\x1b[0m'; // yellow - message - reset
// } else {
// // don't know what that is, leave as is
// }
error(message: string, code: ErrorCode = ErrorCode.ERROR): Log {
if (code == null || code === 0) {
throw new Error("Non-zero error code required.");
}
return s;
this.log.push(new LogItem(message, code, LogLevels.ERROR));
return this;
}
fatal(message: string, code: ErrorCode = ErrorCode.ERROR): Log {
if (code == null || code === 0) {
throw new Error("Non-zero error code required.");
}
if (this._exitCode !== 0) {
throw new Error("Exit code overwritten. Should only have one fatal error.");
}
this._exitCode = code;
this.log.push(new LogItem(message, code, LogLevels.FATAL));
return this;
}
log(message: string, level: LogLevels = LogLevels.INFO, validationError?: ValidationErrors, details?: unknown): void {
if (!validationError) {
validationError = ValidationErrors.UNKNOWN;
}
if (this._prefix) {
message = level.toString() + ": " + message;
}
this._logEntries.push({
level: level,
validationError: validationError,
message: message,
details: details
get(level: LogLevels): LogItem[] {
return this.log.filter(item => {
return item.logLevel === level;
});
if (level >= this._verbosity) {
// print to console
if (level == LogLevels.DEBUG || level == LogLevels.INFO) {
this._console.log(message);
} else if (level == LogLevels.WARNING) {
this._console.log(this.color(message, 'yellow'));
// this._console.log('\x1b[33m%s\x1b[0m', message); // yellow - message - reset
} else if (level == LogLevels.ERROR || level == LogLevels.FATAL) {
this._console.log(this.color(message, 'red'));
// this._console.log('\x1b[31m%s\x1b[0m', message); // red - message - reset
}
if (details != null) {
// log details on a separate call to avoid stringification
this._console.log(details);
}
}
}
error(message: string, level: LogLevels = LogLevels.ERROR, details?: unknown) {
log(message, level, undefined, details);
// collects errors from all children into a single collection; specify level to filter >= level
flatten(level: LogLevels = LogLevels.DEBUG): { title: string, message: string, code: ErrorCode, level: LogLevels }[] {
let items = this.log
.filter((item) => {
return item.logLevel >= level;
})
.map(e => {
return {
title: this.title,
message: e.message,
code: e.code,
level: e.logLevel
};
});
if (this.child) items = items.concat(this.child.flatten(level));
return items;
}
}
export const logger = new Logger(LogLevels.WARNING /* 'out.log'*/);
export default function log(message: string, level: LogLevels = LogLevels.INFO, validationError?: ValidationErrors, details?: unknown) : void {
return logger.log(message, level, validationError, details);
}
const logWrapper = function (level: LogLevels) {
function f(message: string, validationError?: ValidationErrors, details?: unknown): void {
return logger.log(message, level, validationError, details);
}
return f;
};
log.debug = logWrapper(LogLevels.DEBUG);
log.error = logWrapper(LogLevels.ERROR);
log.fatal = logWrapper(LogLevels.FATAL);
log.info = logWrapper(LogLevels.INFO);
log.warn = logWrapper(LogLevels.WARNING);
log.setLevel = function (level: LogLevels) {
logger.verbosity = level;
};
log.setPrefix = function (on: boolean) {
logger.prefix = on;
};

108
src/qr.ts
Просмотреть файл

@ -5,8 +5,9 @@ import svg2img from 'svg2img'; // svg files to image buffer
import { PNG } from 'pngjs'; // png image file reader
import jsQR from 'jsqr'; // qr image decoder
import core from 'file-type/core';
import {OutputTree, ResultWithErrors, ErrorCode, LogItem} from './error';
import { ErrorCode } from './error';
import * as jws from './jws-compact';
import { Log } from './logger';
interface FileInfo {
@ -20,65 +21,79 @@ interface FileInfo {
}
export async function validate(qrSvg: FileInfo): Promise<OutputTree> {
export async function validate(qrSvg: FileInfo): Promise<{result: JWS | undefined, log :Log}> {
const output = new OutputTree('QR code (' + (qrSvg.fileType as string) + ')');
const log = new Log('QR code (' + (qrSvg.fileType as string) + ')');
const results = await decode(qrSvg);
const results : JWS | undefined = await decode(qrSvg, log);
output.add(results.errors);
results && await jws.validate(results);
if (results.result != null) {
output.child = await jws.validate(results.result);
}
return output;
return { result: results, log : log };
}
// the svg data is turned into an image buffer. these values ensure that the resulting image is readable
// by the QR image decoder.
const svgImageWidth = 600;
const svgImageHeight = 600;
const svgImageQuality = 100;
// TODO: find minimal values that cause the resulting image to fail decoding.
// Converts a SVG file into a QR image buffer (as if read from a image file)
async function svgToImageBuffer(svgPath: string): Promise<Buffer> {
async function svgToImageBuffer(svgPath: string, log: Log): Promise<Buffer> {
// TODO: create a test that causes failure here
return new Promise<Buffer>((resolve, reject) => {
svg2img(svgPath, { width: 600, height: 600, quality: 100 }, function (error: unknown, buffer: Buffer) {
if (error) reject(error);
resolve(buffer);
});
svg2img(svgPath, { width: svgImageWidth, height: svgImageHeight, quality: svgImageQuality },
(error: unknown, buffer: Buffer) => {
if (error) {
log.fatal("Could not convert SVG to image. Error: " + (error as Error).message);
reject(undefined);
}
resolve(buffer);
});
});
}
// Decode QR image buffer to base64 string
function decodeQrBuffer(image: Buffer): ResultWithErrors {
function decodeQrBuffer(image: Buffer, log: Log): JWS | undefined {
const result = new ResultWithErrors();
const result: JWS | undefined = undefined;
const png = PNG.sync.read(image);
// TODO : create a test that causes failure here
const code = jsQR(new Uint8ClampedArray(png.data.buffer), png.width, png.height);
if (code == null) {
result.errors.push(new LogItem("Could not decode QR image.", ErrorCode.QR_DECODE_ERROR));
log.fatal("Could not decode QR image.", ErrorCode.QR_DECODE_ERROR);
return result;
}
return shcToJws(code.data);
return shcToJws(code.data, log);
}
function shcToJws(shc: string): ResultWithErrors {
const result = new ResultWithErrors();
if (!/^shc:\/\d+$/g.test(shc)) {
return result.error("Invalid 'shc:/' header string", ErrorCode.INVALID_SHC_STRING);
}
function shcToJws(shc: string, log: Log): JWS | undefined {
const b64Offset = '-'.charCodeAt(0);
const digitPairs = shc.match(/(\d\d?)/g);
// check the header, we can still process if the header is wrong.
if (!/^shc:\//.test(shc)) {
log.error("Invalid 'shc:/' header string", ErrorCode.INVALID_SHC_STRING);
}
// check
const digitPairs = shc.match(/(\d\d)+$/g);
if (digitPairs == null) {
return result.error("Invalid 'shc:/' header string", ErrorCode.INVALID_SHC_STRING);
// we cannot continue without any data
log.fatal("Invalid shc data. Data should be an even numbered string of digits ([0-9][0-9])+", ErrorCode.INVALID_SHC_STRING);
return undefined;
}
// breaks string array of digit pairs into array of numbers: 'shc:/123456...' = [12,34,56]
@ -88,38 +103,39 @@ function shcToJws(shc: string): ResultWithErrors {
// merge the array into a single base64 string
.join('');
result.result = jws;
return result;
return jws;
}
// takes file path to QR data and returns base64 data
async function decode(fileInfo: FileInfo): Promise<ResultWithErrors> {
async function decode(fileInfo: FileInfo, log: Log): Promise<string | undefined> {
let svgBuffer;
const result = new ResultWithErrors();
//const result = new ResultWithErrors();
switch (fileInfo.fileType) {
case 'svg':
svgBuffer = await svgToImageBuffer(fileInfo.buffer.toString());
return decodeQrBuffer(svgBuffer);
case 'svg':
svgBuffer = await svgToImageBuffer(fileInfo.buffer.toString(), log);
return svgBuffer && decodeQrBuffer(svgBuffer, log);
case 'shc':
return Promise.resolve(shcToJws(fileInfo.buffer.toString()));
case 'shc':
return Promise.resolve(shcToJws(fileInfo.buffer.toString(), log));
case 'png':
return decodeQrBuffer(fileInfo.buffer);
case 'png':
return decodeQrBuffer(fileInfo.buffer, log);
case 'jpg':
return result.error("jpg : Not implemented", ErrorCode.NOT_IMPLEMENTED);
case 'jpg':
log.fatal("jpg : Not implemented", ErrorCode.NOT_IMPLEMENTED);
return undefined;
case 'bmp':
return result.error("bmp : Not implemented", ErrorCode.NOT_IMPLEMENTED);
case 'bmp':
log.fatal("bmp : Not implemented", ErrorCode.NOT_IMPLEMENTED);
return undefined;
default:
return result.error("Unknown data in file", ErrorCode.UNKNOWN_FILE_DATA);
default:
log.fatal("Unknown data in file", ErrorCode.UNKNOWN_FILE_DATA);
return undefined;
}
}

Просмотреть файл

@ -3,19 +3,20 @@
import fs from 'fs';
import path from 'path';
import log, { LogLevels } from './logger';
import { ErrorCode, LogItem } from './error';
import { Log } from './logger';
import { ErrorCode } from './error';
// http://json-schema.org/
// https://github.com/ajv-validator/ajv
import JsonValidator, { AnySchemaObject, ErrorObject } from "ajv";
import JsonValidator, { AnySchemaObject } from "ajv";
export async function validateFromFile(schemaPath: string, data: FhirBundle | JWS | JWSPayload | HealthCard): Promise<LogItem[]> {
export async function validateFromFile(schemaPath: string, data: FhirBundle | JWS | JWSPayload | HealthCard, log: Log): Promise<boolean> {
if (!fs.existsSync(schemaPath)) {
log('Schema file not found : ' + schemaPath, LogLevels.FATAL);
return [new LogItem('Schema: file not found : ' + schemaPath, ErrorCode.SCHEMA_FILE_NOT_FOUND)];
log.fatal('Schema file not found : ' + schemaPath, ErrorCode.SCHEMA_ERROR);
return false;
}
const schemaDir = path.basename(path.dirname(schemaPath));
@ -33,54 +34,48 @@ export async function validateFromFile(schemaPath: string, data: FhirBundle | JW
const schemaObj = JSON.parse(fs.readFileSync(schemaPath, 'utf8')) as AnySchemaObject;
return jsonValidator.compileAsync(schemaObj)
.then(validate => {
if (validate(data)) {
return [];
} else {
const errors = (validate.errors as ErrorObject[]);
const outErrors = errors.map(c => {
return new LogItem(
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
'Schema: ' + c.schemaPath + ' \'' + c.message + '\'',
ErrorCode.SCHEMA_ERROR);
});
return outErrors;
}
if (validate(data)) { return true; }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
validate.errors!.forEach(ve => {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
log.error('Schema: ' + ve.schemaPath + ' \'' + ve.message + '\'', ErrorCode.SCHEMA_ERROR);
});
return false;
})
.catch(error => {
return [new LogItem(
"Error: validating against schema : " + (error as Error).message,
ErrorCode.SCHEMA_ERROR)];
.catch(err => {
// TODO: get to this catch in test
log.error('Schema: ' + (err as Error).message, ErrorCode.SCHEMA_ERROR);
return false;
});
}
export function validateSchema(schema: AnySchemaObject, data: FhirBundle | JWS | JWSPayload | HealthCard): LogItem[] {
export function validateSchema(schema: AnySchemaObject, data: FhirBundle | JWS | JWSPayload | HealthCard, log: Log): boolean {
// by default, the validator will stop at the first failure. 'allErrors' allows it to keep going.
const jsonValidator = new JsonValidator({ allErrors: true });
try {
// TODO: make this fail in test
const validate = jsonValidator.compile(schema);
if (validate(data)) { return []; }
if (validate(data)) { return true; }
const validationErrors = (validate.errors as ErrorObject[]);
const outErrors = validationErrors.map(ve => {
return new LogItem(
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
'Schema: ' + ve.schemaPath + ' \'' + ve.message + '\'',
ErrorCode.SCHEMA_ERROR, LogLevels.ERROR);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
validate.errors!.forEach(ve => {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
log.error('Schema: ' + ve.schemaPath + ' \'' + ve.message + '\'', ErrorCode.SCHEMA_ERROR);
});
return outErrors;
return false;
} catch (err) {
return [new LogItem('Schema: ' + (err as Error).message, ErrorCode.SCHEMA_ERROR)];
// TODO: get to this catch in test
log.error('Schema: ' + (err as Error).message, ErrorCode.SCHEMA_ERROR);
return false;
}
}
}

Просмотреть файл

@ -6,7 +6,7 @@ import path from 'path';
import fs from 'fs';
import { Option, Command } from 'commander';
import * as validator from './validate';
import log, { LogLevels } from './logger';
import { LogLevels, Log } from './logger';
import { getFileData } from './file';
import { ErrorCode } from './error';
import * as keys from './keys';
@ -28,6 +28,7 @@ program.option('-o, --logout <path>', 'output path for log (if not specified log
program.option('-k, --jwkset <key>', 'path to trusted issuer key set');
program.parse(process.argv);
interface Options {
path: string;
type: validator.ValidationType;
@ -36,6 +37,10 @@ interface Options {
logout: string;
}
const log = new Log('main');
/**
* Processes the program options and launches validation
*/
@ -46,16 +51,16 @@ async function processOptions() {
// verify that the directory of the logfile exists
if (options.logout) {
const logDir = path.dirname(path.resolve(options.logout));
if(!fs.existsSync(logDir)){
if (!fs.existsSync(logDir)) {
log.fatal('Cannot create log file at: ' + logDir);
return;
}
logFilePathIsValid = true;
}
if (options.loglevel) {
log.setLevel(loglevelChoices.indexOf(options.loglevel) as LogLevels);
}
// if (options.loglevel) {
// log.setLevel(loglevelChoices.indexOf(options.loglevel) as LogLevels);
// }
if (options.path && options.type) {
// read the file to validate
@ -78,29 +83,28 @@ async function processOptions() {
if (options.type === 'jwkset') {
// validate a key file
await validator.validateKey(fileData.buffer);
await validator.validateKey(fileData.buffer, log);
} else {
// validate a health card
const output = await validator.validateCard(fileData, options.type);
process.exitCode = output.exitCode;
process.exitCode = output.log.exitCode;
const level = loglevelChoices.indexOf(options.loglevel) as LogLevels;
// append to the specified logfile
if(logFilePathIsValid) {
if (logFilePathIsValid) {
const out = {
"time" : new Date().toString(),
"options" : options,
"log" : output.flatten(level)
"time": new Date().toString(),
"options": options,
"log": output.log.flatten(level)
};
fs.appendFileSync(options.logout, JSON.stringify(out, null, 4) + '\n');
fs.appendFileSync(options.logout, '\n--------------------------------------------------------------------------------');
} else {
if (output != null) {
log(validator.formatOutput(output, '').join('\n'), level);
}
console.log(validator.formatOutput(output.log, '', level).join('\n'));
}
}

Просмотреть файл

@ -1,30 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/* eslint-disable @typescript-eslint/no-explicit-any */
/* Validate SMART Health Card JSON Web Keys (JWK) */
import log, {LogLevels} from './logger';
import { Log } from './logger';
import jose, { JWK } from 'node-jose';
import { ErrorCode } from './error';
export enum KeyValidationErrors {
INVALID_MISSING_KTY,
INVALID_WRONG_KTY,
INVALID_MISSING_ALG,
INVALID_WRONG_ALG,
INVALID_MISSING_USE,
INVALID_WRONG_USE,
INVALID_MISSING_KID,
INVALID_WRONG_KID,
INVALID_SCHEMA,
INVALID_UNKNOWN
}
export class shcKeyValidator {
async verifyHealthCardIssuerKey(jwk: string | Buffer): Promise<KeyValidationErrors[]> {
const validationResult : KeyValidationErrors[] = [];
async verifyHealthCardIssuerKey(jwk: string | Buffer): Promise<Log> {
const log: Log = new Log('IssuerKey');
const keyStore = JWK.createKeyStore();
return keyStore.add(jwk)
@ -32,58 +18,48 @@ export class shcKeyValidator {
.then(async key => {
// check that key type is 'EC'
if (!key.kty) {
log("'kty' missing in issuer key", LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_MISSING_KTY);
log.error("'kty' missing in issuer key", ErrorCode.INVALID_MISSING_KTY);
} else if (key.kty !== 'EC') {
log("wrong key type in issuer key. expected: 'EC', actual: " + key.kty, LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_WRONG_KTY);
log.error("wrong key type in issuer key. expected: 'EC', actual: " + key.kty, ErrorCode.INVALID_WRONG_KTY);
}
// check that EC curve is 'ES256'
if (!key.alg) {
log("'alg' missing in issuer key", LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_MISSING_ALG);
log.error("'alg' missing in issuer key", ErrorCode.INVALID_MISSING_ALG);
} else if (key.alg !== 'ES256') {
log("wrong algorithm in issuer key. expected: 'ES256', actual: " + key.alg, LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_WRONG_ALG);
log.error("wrong algorithm in issuer key. expected: 'ES256', actual: " + key.alg, ErrorCode.INVALID_WRONG_ALG);
}
// check that usage is 'sig'
if (!key.use) {
log("'use' missing in issuer key", LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_MISSING_USE);
log.error("'use' missing in issuer key", ErrorCode.INVALID_MISSING_USE);
} else if (key.use !== 'sig') {
log("wrong usage in issuer key. expected: 'sig', actual: " + key.use, LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_WRONG_USE);
log.error("wrong usage in issuer key. expected: 'sig', actual: " + key.use, ErrorCode.INVALID_WRONG_USE);
}
// check that kid is properly generated
if (!key.kid) {
log("'kid' missing in issuer key", LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_MISSING_KID);
log.error("'kid' missing in issuer key", ErrorCode.INVALID_MISSING_KID);
} else {
await key.thumbprint('SHA-256')
.then(tpDigest => {
const thumbprint = jose.util.base64url.encode(tpDigest);
if (key.kid !== thumbprint) {
log("'kid' does not match thumbprint in issuer key. expected: "
+ thumbprint + ", actual: " + key.kid, LogLevels.ERROR);
validationResult.push(KeyValidationErrors.INVALID_WRONG_KID);
log.error("'kid' does not match thumbprint in issuer key. expected: "
+ thumbprint + ", actual: " + key.kid, ErrorCode.INVALID_WRONG_KID);
}
})
.catch(err => {
log("Failed to calculate issuer key thumbprint", LogLevels.ERROR, err);
validationResult.push(KeyValidationErrors.INVALID_UNKNOWN);
log.error("Failed to calculate issuer key thumbprint : " + (err as Error).message, ErrorCode.INVALID_UNKNOWN);
});
}
return validationResult;
return log;
})
.catch(err => {
log("Failed to parse issuer key", LogLevels.ERROR, err);
validationResult.push(KeyValidationErrors.INVALID_SCHEMA);
return validationResult;
log.error("Failed to parse issuer key : " + (err as Error).message, ErrorCode.INVALID_SCHEMA);
return log;
});
}
}
}

Просмотреть файл

@ -1,11 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import log, { LogLevels, logger } from './logger';
import { LogItem, LogLevels, Log } from './logger';
import color from 'colors';
import { shcKeyValidator } from './shcKeyValidator';
import { FileInfo } from './file';
import { ErrorCode, LogItem, OutputTree } from './error';
import { ErrorCode } from './error';
import * as healthCard from './healthCard';
import * as jws from './jws-compact';
import * as jwsPayload from './jws-payload';
@ -13,7 +13,6 @@ import * as fhirBundle from './fhirBundle';
import * as qr from './qr';
function list(title: string, items: LogItem[], indent: string, color: (c: string) => string) {
const results: string[] = [];
@ -33,33 +32,35 @@ function list(title: string, items: LogItem[], indent: string, color: (c: string
return results;
}
export function formatOutput(outputTree: OutputTree, indent: string): string[] {
export function formatOutput(outputTree: Log, indent: string, level : LogLevels): string[] {
let results: string[] = [];
results.push(indent + color.bold(outputTree.title));
indent = ' ' + indent;
switch (logger.verbosity) {
case LogLevels.DEBUG:
results = results.concat(list("Debug", outputTree.get(LogLevels.DEBUG), indent + ' ', color.gray));
switch (level) {
case LogLevels.DEBUG:
results = results.concat(list("Debug", outputTree.get(LogLevels.DEBUG), indent + ' ', color.gray));
// eslint-disable-next-line no-fallthrough
case LogLevels.INFO:
results = results.concat(list("Info", outputTree.get(LogLevels.INFO), indent + ' ', color.white.dim ));
case LogLevels.INFO:
results = results.concat(list("Info", outputTree.get(LogLevels.INFO), indent + ' ', color.white.dim));
// eslint-disable-next-line no-fallthrough
case LogLevels.WARNING:
results = results.concat(list("Warning", outputTree.get(LogLevels.WARNING), indent + ' ', color.yellow));
case LogLevels.WARNING:
results = results.concat(list("Warning", outputTree.get(LogLevels.WARNING), indent + ' ', color.yellow));
// eslint-disable-next-line no-fallthrough
case LogLevels.ERROR:
results = results.concat(list("Error", outputTree.get(LogLevels.ERROR), indent + ' ', color.red));
case LogLevels.ERROR:
results = results.concat(list("Error", outputTree.get(LogLevels.ERROR), indent + ' ', color.red));
// eslint-disable-next-line no-fallthrough
case LogLevels.FATAL:
results = results.concat(list("Fatal", outputTree.get(LogLevels.FATAL), indent + ' ', color.red.inverse));
case LogLevels.FATAL:
results = results.concat(list("Fatal", outputTree.get(LogLevels.FATAL), indent + ' ', color.red.inverse));
}
if (outputTree.child) {
results.push(indent + ' |');
results = results.concat(formatOutput(outputTree.child, indent));
results = results.concat(formatOutput(outputTree.child, indent, level));
} else {
makeLeaf(results);
}
@ -80,59 +81,68 @@ function makeLeaf(items: string[]) {
/** Validate the issuer key */
export async function validateKey(key: Buffer): Promise<void> {
export async function validateKey(key: Buffer, log: Log): Promise<void> {
log.debug('Validating key', undefined, key);
log.debug('Validating key : ' + key.toString('utf-8'));
const keyValidator = new shcKeyValidator();
return keyValidator.verifyHealthCardIssuerKey(key)
return keyValidator
.verifyHealthCardIssuerKey(key)
.then(() => { return Promise.resolve(); })
.catch(err => {
log.error("Error validating issuer key", undefined, err);
log.error("Error validating issuer key : " + (err as Error).message);
return Promise.reject();
});
}
export type ValidationType = "qr" | "qrnumeric" | "healthcard" | "jws" | "jwspayload" | "fhirbundle" | "jwkset";
/** Validates SMART Health Card */
export async function validateCard(fileData: FileInfo, type: ValidationType): Promise<OutputTree> {
let output: OutputTree | undefined = undefined;
export interface ValidationResult {
result : HealthCard | JWS | JWSPayload | FhirBundle | undefined,
log : Log
}
/** Validates SMART Health Card */
export async function validateCard(fileData: FileInfo, type: ValidationType): Promise<ValidationResult> {
let result: ValidationResult;
switch (type.toLocaleLowerCase()) {
case "qr":
output = await qr.validate(fileData);
break;
case "qr":
result = await qr.validate(fileData);
break;
case "qrnumeric":
output = await qr.validate(fileData);
break;
case "qrnumeric":
result = await qr.validate(fileData);
break;
case "healthcard":
output = await healthCard.validate(fileData.buffer.toString());
if (fileData.ext !== '.smart-health-card') {
output.warn("Invalid file extenion. Should be .smart-health-card.", ErrorCode.INVALID_FILE_EXTENSION);
}
break;
case "healthcard":
result = await healthCard.validate(fileData.buffer.toString());
if (fileData.ext !== '.smart-health-card') {
result.log.warn("Invalid file extenion. Should be .smart-health-card.", ErrorCode.INVALID_FILE_EXTENSION);
}
break;
case "jws":
output = await jws.validate(fileData.buffer.toString());
break;
case "jws":
result = await jws.validate(fileData.buffer.toString());
break;
case "jwspayload":
output = jwsPayload.validate(fileData.buffer.toString());
break;
case "jwspayload":
result = jwsPayload.validate(fileData.buffer.toString());
break;
case "fhirbundle":
output = fhirBundle.validate(fileData.buffer.toString());
break;
case "fhirbundle":
result = fhirBundle.validate(fileData.buffer.toString());
break;
default:
return Promise.reject("Invalid type : " + type);
default:
return Promise.reject("Invalid type : " + type);
}
return output;
return result;
}

Просмотреть файл

@ -9,10 +9,10 @@ import { LogLevels } from '../src/logger';
const testdataDir = './testdata/';
async function testCard(fileName: string, fileType: ValidationType = 'healthcard', levels : LogLevels[] = [LogLevels.ERROR, LogLevels.FATAL]): Promise<{ title: string, message: string, code: ErrorCode }[]> {
async function testCard(fileName: string, fileType: ValidationType = 'healthcard', levels: LogLevels[] = [LogLevels.ERROR, LogLevels.FATAL]): Promise<{ title: string, message: string, code: ErrorCode }[]> {
const filePath = path.join(testdataDir, fileName);
const outputTree = await validateCard(await getFileData(filePath), fileType);
return outputTree.flatten().filter(i=>{return levels.includes(i.level);});
const log = (await validateCard(await getFileData(filePath), fileType)).log;
return log.flatten().filter(i => { return levels.includes(i.level); });
}
// Test valid examples from spec
@ -31,8 +31,8 @@ test("Cards: valid 01 JWS", async () => expect(await testCard('example-01-d-jws.
test("Cards: valid 00 health card", async () => expect(await testCard('example-00-e-file.smart-health-card', "healthcard")).toHaveLength(0));
test("Cards: valid 01 health card", async () => expect(await testCard('example-01-e-file.smart-health-card', "healthcard")).toHaveLength(0));
test("Cards: valid 00 QR numeric", async () => expect(await testCard('example-00-f-qr-code-numeric.txt', "qrnumeric")).toHaveLength(0));
test("Cards: valid 01 QR numeric", async () => expect(await testCard('example-01-f-qr-code-numeric.txt', "qrnumeric")).toHaveLength(0));
test("Cards: valid 00 QR numeric", async () => expect(await testCard('example-00-f-qr-code-numeric-value-0.txt', "qrnumeric")).toHaveLength(0));
test("Cards: valid 01 QR numeric", async () => expect(await testCard('example-01-f-qr-code-numeric-value-0.txt', "qrnumeric")).toHaveLength(0));
test("Cards: valid 00 QR code", async () => expect(await testCard('example-00-g-qr-code-0.svg', "qr")).toHaveLength(0));
test("Cards: valid 01 QR code", async () => expect(await testCard('example-01-g-qr-code-0.svg', "qr")).toHaveLength(0));

Просмотреть файл

@ -50,12 +50,12 @@ test("Cards: valid 00 health card", () => expect(testCliCommand('node . --path t
test("Cards: valid 00 jws", () => expect(testCliCommand('node . --path testdata/example-00-d-jws.txt --type jws --loglevel info')).toBe(0));
test("Cards: valid 00 jws-payload", () => expect(testCliCommand('node . --path testdata/example-00-c-jws-payload-minified.json --type jwspayload --loglevel info')).toBe(0));
test("Cards: valid 00 fhirBundle", () => expect(testCliCommand('node . --path testdata/example-00-a-fhirBundle.json --type fhirbundle --loglevel info')).toBe(0));
test("Cards: valid 00 qr-code-numeric", () => expect(testCliCommand('node . --path testdata/example-00-f-qr-code-numeric.txt --type qr --loglevel info')).toBe(0));
test("Cards: valid 00 qr-code-numeric", () => expect(testCliCommand('node . --path testdata/example-00-f-qr-code-numeric-value-0.txt --type qr --loglevel info')).toBe(0));
test("Cards: valid 01 health card", () => expect(testCliCommand('node . --path testdata/example-01-e-file.smart-health-card --type healthcard --loglevel warning')).toBe(0));
test("Cards: valid 01 jws", () => expect(testCliCommand('node . --path testdata/example-01-d-jws.txt --type jws --loglevel warning')).toBe(0));
test("Cards: valid 01 jws-payload", () => expect(testCliCommand('node . --path testdata/example-01-c-jws-payload-minified.json --type jwspayload --loglevel warning')).toBe(0));
test("Cards: valid 01 fhirBundle", () => expect(testCliCommand('node . --path testdata/example-01-a-fhirBundle.json --type fhirbundle --loglevel warning')).toBe(0));
test("Cards: valid 01 r-code-numeric", () => expect(testCliCommand('node . --path testdata/example-01-f-qr-code-numeric.txt --type qrnumeric --loglevel info')).toBe(0));
test("Cards: valid 01 r-code-numeric", () => expect(testCliCommand('node . --path testdata/example-01-f-qr-code-numeric-value-0.txt --type qrnumeric --loglevel info')).toBe(0));
test("Cards: valid 01 qr-code.svg", () => expect(testCliCommand('node . --path testdata/example-00-g-qr-code-0.svg --type qr --loglevel info')).toBe(0));
test("Cards: valid qr.png", () => expect(testCliCommand('node . --path testdata/qr.png --type qr --loglevel info')).toBe(0));
test("Cards: valid qr-90.pngd", () => expect(testCliCommand('node . --path testdata/qr-90.png --type qr --loglevel info')).toBe(0));

Просмотреть файл

@ -3,29 +3,37 @@
import fs from 'fs';
import path from 'path';
import {shcKeyValidator, KeyValidationErrors} from '../src/shcKeyValidator';
import { ErrorCode } from '../src/error';
import { shcKeyValidator } from '../src/shcKeyValidator';
const testdataDir = './testdata/';
async function testKey(fileName: string): Promise<KeyValidationErrors[]> {
async function testKey(fileName: string): Promise<ErrorCode[]> {
const filePath = path.join(testdataDir, fileName);
const keyValidator = new shcKeyValidator();
return await keyValidator.verifyHealthCardIssuerKey(fs.readFileSync(filePath));
const log = (await keyValidator.verifyHealthCardIssuerKey(fs.readFileSync(filePath))).log;
return log.map(item => item.code);
}
test("Keys: valid", async () => {
expect(await testKey('valid_key.json')).toHaveLength(0);});
expect(await testKey('valid_key.json')).toHaveLength(0);
});
test("Keys: wrong key identifier (kid)", async () => {
expect(await testKey('wrong_kid_key.json')).toContain(KeyValidationErrors.INVALID_WRONG_KID);});
expect(await testKey('wrong_kid_key.json')).toContain(ErrorCode.INVALID_WRONG_KID);
});
test("Keys: wrong elliptic curve", async () => {
expect(await testKey('wrong_curve_key.json')).toContain(KeyValidationErrors.INVALID_WRONG_ALG);});
expect(await testKey('wrong_curve_key.json')).toContain(ErrorCode.INVALID_WRONG_ALG);
});
test("Keys: wrong key use (use)", async () => {
expect(await testKey('wrong_use_key.json')).toContain(KeyValidationErrors.INVALID_WRONG_USE);});
test("Keys: wrong key use (use)", async () => {
expect(await testKey('wrong_use_key.json')).toContain(ErrorCode.INVALID_WRONG_USE);
});
test("Keys: wrong algorithm (alg)", async () => {
expect(await testKey('wrong_alg_key.json')).toContain(KeyValidationErrors.INVALID_WRONG_ALG);});
expect(await testKey('wrong_alg_key.json')).toContain(ErrorCode.INVALID_WRONG_ALG);
});
test("Keys: wrong key type (kty)", async () => {
expect(await testKey('wrong_kty_key.json')).toContain(KeyValidationErrors.INVALID_WRONG_KTY);});
expect(await testKey('wrong_kty_key.json')).toContain(ErrorCode.INVALID_WRONG_KTY);
});

Просмотреть файл

@ -3,6 +3,7 @@
import fs from 'fs';
import path from 'path';
import { LogLevels, Log } from '../src/logger';
import { validateFromFile } from '../src/schema';
@ -74,10 +75,12 @@ function testSchema(exampleFile: string): void {
const fileData = fs.readFileSync(examplePath, 'utf-8');
const ext = path.extname(examplePath);
const dataObj = ext !== '.txt' ? JSON.parse(fileData) as FhirBundle | JWS | JWSPayload | HealthCard : fileData;
const log = new Log('testSchema');
test("Schema: " + schemaName + " " + exampleFile, async () => {
const result = await validateFromFile(schemaPath, dataObj);
expect(result.length).toBe(0);
const result = await validateFromFile(schemaPath, dataObj, log);
expect(result).toBe(true);
expect(log.flatten(LogLevels.WARNING).length).toBe(0);
});
return;